Compare commits
13 Commits
ac2b6aba6e
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| c59efaeff9 | |||
| a1c9e3807b | |||
| 6736ce8c13 | |||
| 03940a99ba | |||
| b46ab28615 | |||
| 3486b82879 | |||
| 6a92451776 | |||
| 17b8bafccd | |||
| 0083539bac | |||
| d46a2573c4 | |||
| 3a68531fb4 | |||
| 56273c2e3f | |||
| 0b74cf2daa |
35
.gitea/workflows/02_release.yml
Normal file
35
.gitea/workflows/02_release.yml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Credit: https://gitea.com/gitea/runner-images/src/branch/main/.gitea/workflows/release.yaml
|
||||||
|
name: Release new version
|
||||||
|
run-name: Release ${{ github.ref_name }}
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: docker.io/thegeeklab/git-sv:2.0.9
|
||||||
|
steps:
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
apk add -q --update --no-cache nodejs
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-tags: true
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Create changelog
|
||||||
|
run: |
|
||||||
|
git sv current-version
|
||||||
|
git sv release-notes -t ${GITHUB_REF#refs/tags/} -o CHANGELOG.md
|
||||||
|
sed -i '1,2d' CHANGELOG.md # remove version
|
||||||
|
cat CHANGELOG.md
|
||||||
|
|
||||||
|
- name: Release
|
||||||
|
uses: https://github.com/akkuman/gitea-release-action@v1
|
||||||
|
with:
|
||||||
|
body_path: CHANGELOG.md
|
||||||
|
token: "${{ secrets.REPO_RW_TOKEN }}"
|
||||||
BIN
Assets/vinyl.png
Normal file
BIN
Assets/vinyl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
135
Controllers/AlbumController.cs
Normal file
135
Controllers/AlbumController.cs
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
using Shadow.Data;
|
||||||
|
using Shadow.DTOs;
|
||||||
|
using Shadow.Entities;
|
||||||
|
using Shadow.Mapping;
|
||||||
|
using Shadow.Tools;
|
||||||
|
|
||||||
|
namespace Shadow.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("rest")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class AlbumController : ControllerBase
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly ApplicationDbContext db;
|
||||||
|
private readonly GeneralUseHelpers guhf;
|
||||||
|
|
||||||
|
public AlbumController(ApplicationDbContext _db, GeneralUseHelpers _guhf)
|
||||||
|
{
|
||||||
|
db = _db;
|
||||||
|
guhf = _guhf;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("getCoverArt")]
|
||||||
|
[HttpGet("getCoverArt.view")]
|
||||||
|
[HttpPost("getCoverArt.view")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
[ProducesResponseType(404)]
|
||||||
|
public async Task<IActionResult> GetCoverArt()
|
||||||
|
{
|
||||||
|
string thumbnailPath = db.Globals
|
||||||
|
.First(g => g.Key == "musicThumbnailPath").Value!;
|
||||||
|
|
||||||
|
IFileInfo fileInfo = new PhysicalFileProvider(thumbnailPath)
|
||||||
|
.GetFileInfo("default.png");
|
||||||
|
|
||||||
|
if (!fileInfo.Exists) return NotFound();
|
||||||
|
return PhysicalFile(fileInfo.PhysicalPath!, "image/png");
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("getAlbumList2")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
public async Task<IActionResult> GetAlbumList2()
|
||||||
|
{
|
||||||
|
// First, check if user is authorized
|
||||||
|
User? user = await guhf.GetUserFromParams(Request);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
// Craft an error
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
Status = "failed",
|
||||||
|
error = new ErrorDTO()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AlbumViewShortDTO> albumsDTO = [];
|
||||||
|
List<Album> albums = db.Albums
|
||||||
|
.Where(a => a.State != 1)
|
||||||
|
.Include(a => a.Artist)
|
||||||
|
.Include(a => a.Songs)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (Album al in albums)
|
||||||
|
{
|
||||||
|
albumsDTO.Add(
|
||||||
|
AlbumMapping.ToAlbumViewShort(al)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
albumList2 = new AlbumList2DTO
|
||||||
|
{
|
||||||
|
album = albumsDTO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("getAlbumList2.view")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
public async Task<IActionResult> GetAlbumList2Form()
|
||||||
|
{
|
||||||
|
// First, check if user is authorized
|
||||||
|
User? user = await guhf.GetUserFromForm(Request);
|
||||||
|
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
// Craft an error
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
Status = "failed",
|
||||||
|
error = new ErrorDTO()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<AlbumViewShortDTO> albumsDTO = [];
|
||||||
|
List<Album> albums = db.Albums
|
||||||
|
.Where(a => a.State != 1)
|
||||||
|
.Include(a => a.Artist)
|
||||||
|
.Include(a => a.Songs)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (Album al in albums)
|
||||||
|
{
|
||||||
|
albumsDTO.Add(
|
||||||
|
AlbumMapping.ToAlbumViewShort(al)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
albumList2 = new AlbumList2DTO
|
||||||
|
{
|
||||||
|
album = albumsDTO
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
153
Controllers/MiscController.cs
Normal file
153
Controllers/MiscController.cs
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Shadow.Data;
|
||||||
|
using Shadow.DTOs;
|
||||||
|
using Shadow.Entities;
|
||||||
|
using Shadow.Tools;
|
||||||
|
|
||||||
|
namespace Shadow.Controllers;
|
||||||
|
|
||||||
|
[ApiController]
|
||||||
|
[Route("rest")]
|
||||||
|
[Produces("application/json")]
|
||||||
|
public class MiscController : ControllerBase
|
||||||
|
{
|
||||||
|
|
||||||
|
private readonly ApplicationDbContext db;
|
||||||
|
private readonly GeneralUseHelpers guhf;
|
||||||
|
|
||||||
|
public MiscController(ApplicationDbContext _db, GeneralUseHelpers _guhf)
|
||||||
|
{
|
||||||
|
db = _db;
|
||||||
|
guhf = _guhf;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("ping")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
public async Task<IActionResult> Ping()
|
||||||
|
{
|
||||||
|
User? user = await guhf.GetUserFromParams(Request);
|
||||||
|
|
||||||
|
// Wrong credentials?
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
// Craft an error
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
Status = "failed",
|
||||||
|
error = new ErrorDTO
|
||||||
|
{
|
||||||
|
code = 40,
|
||||||
|
message = "Wrong username or password."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseWrapper {
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("ping.view")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
public async Task<IActionResult> PingForm()
|
||||||
|
{
|
||||||
|
User? user = await guhf.GetUserFromForm(Request);
|
||||||
|
|
||||||
|
// Wrong credentials?
|
||||||
|
if (user == null) {
|
||||||
|
// Craft an error
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO {
|
||||||
|
Status = "failed",
|
||||||
|
error = new ErrorDTO {
|
||||||
|
code = 40,
|
||||||
|
message = "Wrong username or password."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("getUser")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
public async Task<IActionResult> GetUser()
|
||||||
|
{
|
||||||
|
User? user = await guhf.GetUserFromParams(Request);
|
||||||
|
|
||||||
|
// Wrong credentials?
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
// Craft an error
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
Status = "failed",
|
||||||
|
error = new ErrorDTO
|
||||||
|
{
|
||||||
|
code = 40,
|
||||||
|
message = "Wrong username or password."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
userPing = new UserPingDTO
|
||||||
|
{
|
||||||
|
username = user.Name,
|
||||||
|
adminRole = user.IsAdmin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("getUser.view")]
|
||||||
|
[ProducesResponseType(200)]
|
||||||
|
public async Task<IActionResult> GetUserForm()
|
||||||
|
{
|
||||||
|
User? user = await guhf.GetUserFromForm(Request);
|
||||||
|
|
||||||
|
// Wrong credentials?
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
// Craft an error
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
Status = "failed",
|
||||||
|
error = new ErrorDTO
|
||||||
|
{
|
||||||
|
code = 40,
|
||||||
|
message = "Wrong username or password."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new ResponseWrapper
|
||||||
|
{
|
||||||
|
SubsonicResponse = new SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
userPing = new UserPingDTO
|
||||||
|
{
|
||||||
|
username = user.Name,
|
||||||
|
adminRole = user.IsAdmin()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
11
DTOs/AlbumList2DTO.cs
Normal file
11
DTOs/AlbumList2DTO.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace Shadow.DTOs;
|
||||||
|
|
||||||
|
public class AlbumList2DTO
|
||||||
|
{
|
||||||
|
[JsonPropertyName("album")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public List<AlbumViewShortDTO>? album { get; set; } = null;
|
||||||
|
}
|
||||||
51
DTOs/AlbumViewShortDTO.cs
Normal file
51
DTOs/AlbumViewShortDTO.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace Shadow.DTOs;
|
||||||
|
|
||||||
|
public class AlbumViewShortDTO
|
||||||
|
{
|
||||||
|
[JsonPropertyName("id")]
|
||||||
|
public string id { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("name")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? name { get; set; } = null;
|
||||||
|
|
||||||
|
[JsonPropertyName("artist")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? artist { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("artistId")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? artistId { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[JsonPropertyName("coverArt")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? coverArt { get; set; } = "default.png";
|
||||||
|
|
||||||
|
[JsonPropertyName("songCount")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public int? songCount { get; set; } = null;
|
||||||
|
|
||||||
|
[JsonPropertyName("duration")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public int? duration { get; set; } = 0;
|
||||||
|
|
||||||
|
[JsonPropertyName("playCount")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public int? playCount { get; set; } = 0;
|
||||||
|
|
||||||
|
[JsonPropertyName("year")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public int? year { get; set; } = null;
|
||||||
|
|
||||||
|
[JsonPropertyName("genre")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? genre { get; set; } = null;
|
||||||
|
|
||||||
|
[JsonPropertyName("played")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string? played { get; set; } = null;
|
||||||
|
|
||||||
|
}
|
||||||
13
DTOs/ErrorDTO.cs
Normal file
13
DTOs/ErrorDTO.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace Shadow.DTOs;
|
||||||
|
|
||||||
|
public class ErrorDTO
|
||||||
|
{
|
||||||
|
[JsonPropertyName("code")]
|
||||||
|
public int code { get; set; } = 0;
|
||||||
|
|
||||||
|
[JsonPropertyName("message")]
|
||||||
|
public string message { get; set; } = "Generic error";
|
||||||
|
}
|
||||||
9
DTOs/ResponseWrapper.cs
Normal file
9
DTOs/ResponseWrapper.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Shadow.DTOs;
|
||||||
|
|
||||||
|
public class ResponseWrapper
|
||||||
|
{
|
||||||
|
[JsonPropertyName("subsonic-response")]
|
||||||
|
required public SubsonicResponseDTO SubsonicResponse { get; set; }
|
||||||
|
}
|
||||||
36
DTOs/SubsonicResponseDTO.cs
Normal file
36
DTOs/SubsonicResponseDTO.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
|
namespace Shadow.DTOs;
|
||||||
|
|
||||||
|
public class SubsonicResponseDTO
|
||||||
|
{
|
||||||
|
[JsonPropertyName("status")]
|
||||||
|
public string Status { get; set; } = "ok";
|
||||||
|
|
||||||
|
[JsonPropertyName("version")]
|
||||||
|
public string version { get; set; } = "1.15.0";
|
||||||
|
|
||||||
|
[JsonPropertyName("type")]
|
||||||
|
public string type { get; set; } = "shadow";
|
||||||
|
|
||||||
|
[JsonPropertyName("serverVersion")]
|
||||||
|
public string serverVersion { get; set; } = $"0.0.1 ({ThisAssembly.Git.Commit})";
|
||||||
|
|
||||||
|
[JsonPropertyName("openSubsonic")]
|
||||||
|
public bool openSubsonic { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("error")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public ErrorDTO? error { get; set; } = null;
|
||||||
|
|
||||||
|
[JsonPropertyName("albumList2")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public AlbumList2DTO? albumList2 { get; set; } = null;
|
||||||
|
|
||||||
|
[JsonPropertyName("user")]
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public UserPingDTO? userPing { get; set; } = null;
|
||||||
|
|
||||||
|
}
|
||||||
49
DTOs/UserPingDTO.cs
Normal file
49
DTOs/UserPingDTO.cs
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Shadow.DTOs;
|
||||||
|
|
||||||
|
public class UserPingDTO
|
||||||
|
{
|
||||||
|
[JsonPropertyName("username")]
|
||||||
|
public string? username { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("scrobblingEnabled")]
|
||||||
|
public bool scrobblingEnabled { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("adminRole")]
|
||||||
|
public bool adminRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("settingsRole")]
|
||||||
|
public bool settingsRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("downloadRole")]
|
||||||
|
public bool downlaodRole { get; set; } = true;
|
||||||
|
|
||||||
|
[JsonPropertyName("uploadRole")]
|
||||||
|
public bool uploadRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("playlistRole")]
|
||||||
|
public bool playlistRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("coverArtRole")]
|
||||||
|
public bool coverArtRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("commentRole")]
|
||||||
|
public bool commentRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("podcastRole")]
|
||||||
|
public bool podcastRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("streamRole")]
|
||||||
|
public bool streamRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("jukeboxRole")]
|
||||||
|
public bool jukeboxRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("shareRole")]
|
||||||
|
public bool shareRole { get; set; } = false;
|
||||||
|
|
||||||
|
[JsonPropertyName("videoConversionRole")]
|
||||||
|
public bool videoConversionRole { get; set; } = false;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ public class Playlist
|
|||||||
required public string Uri { get; set; }
|
required public string Uri { get; set; }
|
||||||
required public string Description { get; set; }
|
required public string Description { get; set; }
|
||||||
required public int CreatorId { get; set; } // UserId?
|
required public int CreatorId { get; set; } // UserId?
|
||||||
|
required public bool Private { get; set; } = true;
|
||||||
|
|
||||||
required public User Creator { get; set; }
|
required public User Creator { get; set; }
|
||||||
public List<PlaylistUser> AuthorizedPlaylistUsers { get; set; } = [];
|
public List<PlaylistUser> AuthorizedPlaylistUsers { get; set; } = [];
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ public class Song
|
|||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
public int? TrackNumber { get; set; } = null;
|
public int? TrackNumber { get; set; } = null;
|
||||||
public int? DiscNumber { get; set; } = null;
|
public int? DiscNumber { get; set; } = null;
|
||||||
required public int AlbumId { get; set; }
|
public int AlbumId { get; set; }
|
||||||
required public int ArtistId { get; set; }
|
public int ArtistId { get; set; }
|
||||||
public int? ImageId { get; set; } = null;
|
public int? ImageId { get; set; } = null;
|
||||||
|
|
||||||
// Songs without an album entry shall default to "[Unnamed album]".
|
// Songs without an album entry shall default to "[Unnamed album]".
|
||||||
@@ -31,6 +31,7 @@ public class Song
|
|||||||
public List<GenreSong> GenreSongPair { get; set; } = [];
|
public List<GenreSong> GenreSongPair { get; set; } = [];
|
||||||
public Image? Image { get; set; } = null;
|
public Image? Image { get; set; } = null;
|
||||||
|
|
||||||
|
// TODO: Turn these into an enum.
|
||||||
public bool IsOk() => State == 0;
|
public bool IsOk() => State == 0;
|
||||||
public bool IsOrphaned() => State == 1;
|
public bool IsOrphaned() => State == 1;
|
||||||
public bool IsArchived() => State == 2;
|
public bool IsArchived() => State == 2;
|
||||||
|
|||||||
27
Mapping/AlbumMapping.cs
Normal file
27
Mapping/AlbumMapping.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
using Shadow.DTOs;
|
||||||
|
using Shadow.Entities;
|
||||||
|
|
||||||
|
namespace Shadow.Mapping;
|
||||||
|
|
||||||
|
public static class AlbumMapping
|
||||||
|
{
|
||||||
|
public static AlbumViewShortDTO ToAlbumViewShort(Album album)
|
||||||
|
{
|
||||||
|
AlbumViewShortDTO dto = new AlbumViewShortDTO
|
||||||
|
{
|
||||||
|
id = $"{album.Id}",
|
||||||
|
name = album.Name,
|
||||||
|
artist = album.Artist?.Name ?? "[Unknown Artist]",
|
||||||
|
artistId = $"{album.ArtistId}",
|
||||||
|
coverArt = "default.png",
|
||||||
|
songCount = album.Songs.Count,
|
||||||
|
duration = album.Songs.Sum(s => s.Duration),
|
||||||
|
playCount = 0,
|
||||||
|
year = 0,
|
||||||
|
genre = "unknown",
|
||||||
|
// played = "never"
|
||||||
|
};
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Migrations/00000000000000_StoredProcedure.Designer.cs
generated
Normal file
31
Migrations/00000000000000_StoredProcedure.Designer.cs
generated
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Shadow.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Shadow.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("00000000000000_StoredProcedure")]
|
||||||
|
partial class _00000000000000_StoredProcedure
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.1")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
Migrations/00000000000000_StoredProcedure.cs
Normal file
52
Migrations/00000000000000_StoredProcedure.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
namespace Shadow.Migrations;
|
||||||
|
public partial class _00000000000000_StoredProcedure : Migration
|
||||||
|
{
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
|
||||||
|
CREATE OR REPLACE PROCEDURE song_cleanup()
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
DELETE FROM "Songs"
|
||||||
|
WHERE "State" = 1;
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
|
||||||
|
CREATE OR REPLACE PROCEDURE force_rescan()
|
||||||
|
LANGUAGE plpgsql
|
||||||
|
AS $$
|
||||||
|
BEGIN
|
||||||
|
UPDATE "Globals"
|
||||||
|
SET "Value" = '0'
|
||||||
|
WHERE "Key" = 'libraryState';
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
END;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
DROP PROCEDURE IF EXISTS song_cleanup();
|
||||||
|
""");
|
||||||
|
|
||||||
|
migrationBuilder.Sql("""
|
||||||
|
DROP PROCEDURE IF EXISTS force_rescan();
|
||||||
|
""");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
635
Migrations/20251216025633_InitialMigration.Designer.cs
generated
635
Migrations/20251216025633_InitialMigration.Designer.cs
generated
@@ -1,635 +0,0 @@
|
|||||||
// <auto-generated />
|
|
||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
using Shadow.Data;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Shadow.Migrations
|
|
||||||
{
|
|
||||||
[DbContext(typeof(ApplicationDbContext))]
|
|
||||||
[Migration("20251216025633_InitialMigration")]
|
|
||||||
partial class InitialMigration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
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("Shadow.Entities.Album", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<int?>("ArtistId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("State")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Uri")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("ArtistId");
|
|
||||||
|
|
||||||
b.HasIndex("Uri")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Albums");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.AlbumInteraction", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("AlbumId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<DateTime?>("PlayDate")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<bool>("Starred")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.HasKey("AlbumId", "UserId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("AlbumInteractions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Artist", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Artists");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Genre", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Genres");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.GenreSong", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("GenreId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("SongId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("GenreId", "SongId");
|
|
||||||
|
|
||||||
b.HasIndex("SongId");
|
|
||||||
|
|
||||||
b.ToTable("GenreSongs");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Global", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("Key")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Value")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Key")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Globals");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Image", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("Filetype")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int?>("SongId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("State")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Uri")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("SongId")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Images");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<int>("CreatorId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Description")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Uri")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("CreatorId");
|
|
||||||
|
|
||||||
b.HasIndex("Uri")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Playlists");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.PlaylistSong", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("PlaylistId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("SongId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Index")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("PlaylistId", "SongId");
|
|
||||||
|
|
||||||
b.HasIndex("SongId");
|
|
||||||
|
|
||||||
b.ToTable("PlaylistSongs");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.PlaylistUser", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("PlaylistId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("PlaylistId", "UserId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("PlaylistUsers");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Radio", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("Homepage")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Url")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("NormalizedName")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("Radios");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Song", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<int>("AlbumId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("ArtistId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int?>("BitDepth")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Bitrate")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Channels")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Comment")
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<DateTime>("Date")
|
|
||||||
.HasColumnType("timestamp with time zone");
|
|
||||||
|
|
||||||
b.Property<int?>("DiscNumber")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Duration")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Filepath")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Filetype")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int?>("ImageId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Index")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("SamplingRate")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Size")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("State")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Title")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int?>("TrackNumber")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<string>("Uri")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("AlbumId");
|
|
||||||
|
|
||||||
b.HasIndex("ArtistId");
|
|
||||||
|
|
||||||
b.HasIndex("Uri")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Songs");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.SongInteraction", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("SongId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("UserId")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("PlayCount")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<int>("Rating")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.Property<bool?>("Starred")
|
|
||||||
.HasColumnType("boolean");
|
|
||||||
|
|
||||||
b.HasKey("SongId", "UserId");
|
|
||||||
|
|
||||||
b.HasIndex("UserId");
|
|
||||||
|
|
||||||
b.ToTable("SongInteractions");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.User", b =>
|
|
||||||
{
|
|
||||||
b.Property<int>("Id")
|
|
||||||
.ValueGeneratedOnAdd()
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
|
||||||
|
|
||||||
b.Property<string>("Name")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("NormalizedName")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<string>("Password")
|
|
||||||
.IsRequired()
|
|
||||||
.HasColumnType("text");
|
|
||||||
|
|
||||||
b.Property<int>("Role")
|
|
||||||
.HasColumnType("integer");
|
|
||||||
|
|
||||||
b.HasKey("Id");
|
|
||||||
|
|
||||||
b.HasIndex("Name")
|
|
||||||
.IsUnique();
|
|
||||||
|
|
||||||
b.ToTable("Users");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Album", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.Artist", "Artist")
|
|
||||||
.WithMany("Albums")
|
|
||||||
.HasForeignKey("ArtistId");
|
|
||||||
|
|
||||||
b.Navigation("Artist");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.AlbumInteraction", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.Album", "Album")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("AlbumId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Shadow.Entities.User", "User")
|
|
||||||
.WithMany("AlbumInteractions")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Album");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.GenreSong", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.Genre", "Genre")
|
|
||||||
.WithMany("GenreSongPair")
|
|
||||||
.HasForeignKey("GenreId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Shadow.Entities.Song", "Song")
|
|
||||||
.WithMany("GenreSongPair")
|
|
||||||
.HasForeignKey("SongId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Genre");
|
|
||||||
|
|
||||||
b.Navigation("Song");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Image", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.Song", "Song")
|
|
||||||
.WithOne("Image")
|
|
||||||
.HasForeignKey("Shadow.Entities.Image", "SongId");
|
|
||||||
|
|
||||||
b.Navigation("Song");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.User", "Creator")
|
|
||||||
.WithMany("Playlists")
|
|
||||||
.HasForeignKey("CreatorId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Creator");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.PlaylistSong", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.Playlist", "Playlist")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("PlaylistId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Shadow.Entities.Song", "Song")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SongId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Playlist");
|
|
||||||
|
|
||||||
b.Navigation("Song");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.PlaylistUser", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.Playlist", "Playlist")
|
|
||||||
.WithMany("AuthorizedPlaylistUsers")
|
|
||||||
.HasForeignKey("PlaylistId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Shadow.Entities.User", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Playlist");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Radio", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.User", "User")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Song", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.Album", "Album")
|
|
||||||
.WithMany("Songs")
|
|
||||||
.HasForeignKey("AlbumId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Shadow.Entities.Artist", "Artist")
|
|
||||||
.WithMany("Songs")
|
|
||||||
.HasForeignKey("ArtistId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Album");
|
|
||||||
|
|
||||||
b.Navigation("Artist");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.SongInteraction", b =>
|
|
||||||
{
|
|
||||||
b.HasOne("Shadow.Entities.Song", "Song")
|
|
||||||
.WithMany()
|
|
||||||
.HasForeignKey("SongId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.HasOne("Shadow.Entities.User", "User")
|
|
||||||
.WithMany("SongInteractions")
|
|
||||||
.HasForeignKey("UserId")
|
|
||||||
.OnDelete(DeleteBehavior.Cascade)
|
|
||||||
.IsRequired();
|
|
||||||
|
|
||||||
b.Navigation("Song");
|
|
||||||
|
|
||||||
b.Navigation("User");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Album", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Songs");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Artist", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("Albums");
|
|
||||||
|
|
||||||
b.Navigation("Songs");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Genre", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("GenreSongPair");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("AuthorizedPlaylistUsers");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.Song", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("GenreSongPair");
|
|
||||||
|
|
||||||
b.Navigation("Image");
|
|
||||||
});
|
|
||||||
|
|
||||||
modelBuilder.Entity("Shadow.Entities.User", b =>
|
|
||||||
{
|
|
||||||
b.Navigation("AlbumInteractions");
|
|
||||||
|
|
||||||
b.Navigation("Playlists");
|
|
||||||
|
|
||||||
b.Navigation("SongInteractions");
|
|
||||||
});
|
|
||||||
#pragma warning restore 612, 618
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,482 +0,0 @@
|
|||||||
using System;
|
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|
||||||
|
|
||||||
#nullable disable
|
|
||||||
|
|
||||||
namespace Shadow.Migrations
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
public partial class InitialMigration : Migration
|
|
||||||
{
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Up(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Artists",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
NormalizedName = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Artists", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Genres",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
NormalizedName = table.Column<string>(type: "text", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Genres", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Globals",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Key = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Value = table.Column<string>(type: "text", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Globals", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Users",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
NormalizedName = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Password = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Role = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Users", x => x.Id);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Albums",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Uri = table.Column<string>(type: "text", nullable: false),
|
|
||||||
State = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
ArtistId = table.Column<int>(type: "integer", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Albums", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Albums_Artists_ArtistId",
|
|
||||||
column: x => x.ArtistId,
|
|
||||||
principalTable: "Artists",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Playlists",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Uri = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Description = table.Column<string>(type: "text", nullable: false),
|
|
||||||
CreatorId = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Playlists", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Playlists_Users_CreatorId",
|
|
||||||
column: x => x.CreatorId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Radios",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Name = table.Column<string>(type: "text", nullable: false),
|
|
||||||
NormalizedName = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Homepage = table.Column<string>(type: "text", nullable: true),
|
|
||||||
Url = table.Column<string>(type: "text", nullable: false),
|
|
||||||
UserId = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Radios", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Radios_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "AlbumInteractions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
AlbumId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
UserId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
PlayDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
|
||||||
Starred = table.Column<bool>(type: "boolean", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_AlbumInteractions", x => new { x.AlbumId, x.UserId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AlbumInteractions_Albums_AlbumId",
|
|
||||||
column: x => x.AlbumId,
|
|
||||||
principalTable: "Albums",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_AlbumInteractions_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Songs",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Title = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Uri = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Filepath = table.Column<string>(type: "text", nullable: false),
|
|
||||||
State = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Filetype = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
|
||||||
Duration = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Bitrate = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Size = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Comment = table.Column<string>(type: "text", nullable: true),
|
|
||||||
Channels = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
SamplingRate = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
BitDepth = table.Column<int>(type: "integer", nullable: true),
|
|
||||||
Index = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
TrackNumber = table.Column<int>(type: "integer", nullable: true),
|
|
||||||
DiscNumber = table.Column<int>(type: "integer", nullable: true),
|
|
||||||
AlbumId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
ArtistId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
ImageId = table.Column<int>(type: "integer", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Songs", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Songs_Albums_AlbumId",
|
|
||||||
column: x => x.AlbumId,
|
|
||||||
principalTable: "Albums",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Songs_Artists_ArtistId",
|
|
||||||
column: x => x.ArtistId,
|
|
||||||
principalTable: "Artists",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PlaylistUsers",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
PlaylistId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
UserId = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PlaylistUsers", x => new { x.PlaylistId, x.UserId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PlaylistUsers_Playlists_PlaylistId",
|
|
||||||
column: x => x.PlaylistId,
|
|
||||||
principalTable: "Playlists",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PlaylistUsers_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "GenreSongs",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
GenreId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
SongId = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_GenreSongs", x => new { x.GenreId, x.SongId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_GenreSongs_Genres_GenreId",
|
|
||||||
column: x => x.GenreId,
|
|
||||||
principalTable: "Genres",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_GenreSongs_Songs_SongId",
|
|
||||||
column: x => x.SongId,
|
|
||||||
principalTable: "Songs",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "Images",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
|
||||||
Uri = table.Column<string>(type: "text", nullable: false),
|
|
||||||
Filetype = table.Column<string>(type: "text", nullable: false),
|
|
||||||
State = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
SongId = table.Column<int>(type: "integer", nullable: true)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_Images", x => x.Id);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_Images_Songs_SongId",
|
|
||||||
column: x => x.SongId,
|
|
||||||
principalTable: "Songs",
|
|
||||||
principalColumn: "Id");
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "PlaylistSongs",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
PlaylistId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
SongId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Index = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_PlaylistSongs", x => new { x.PlaylistId, x.SongId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PlaylistSongs_Playlists_PlaylistId",
|
|
||||||
column: x => x.PlaylistId,
|
|
||||||
principalTable: "Playlists",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_PlaylistSongs_Songs_SongId",
|
|
||||||
column: x => x.SongId,
|
|
||||||
principalTable: "Songs",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateTable(
|
|
||||||
name: "SongInteractions",
|
|
||||||
columns: table => new
|
|
||||||
{
|
|
||||||
SongId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
UserId = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Id = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
PlayCount = table.Column<int>(type: "integer", nullable: false),
|
|
||||||
Starred = table.Column<bool>(type: "boolean", nullable: true),
|
|
||||||
Rating = table.Column<int>(type: "integer", nullable: false)
|
|
||||||
},
|
|
||||||
constraints: table =>
|
|
||||||
{
|
|
||||||
table.PrimaryKey("PK_SongInteractions", x => new { x.SongId, x.UserId });
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_SongInteractions_Songs_SongId",
|
|
||||||
column: x => x.SongId,
|
|
||||||
principalTable: "Songs",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
table.ForeignKey(
|
|
||||||
name: "FK_SongInteractions_Users_UserId",
|
|
||||||
column: x => x.UserId,
|
|
||||||
principalTable: "Users",
|
|
||||||
principalColumn: "Id",
|
|
||||||
onDelete: ReferentialAction.Cascade);
|
|
||||||
});
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_AlbumInteractions_UserId",
|
|
||||||
table: "AlbumInteractions",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Albums_ArtistId",
|
|
||||||
table: "Albums",
|
|
||||||
column: "ArtistId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Albums_Uri",
|
|
||||||
table: "Albums",
|
|
||||||
column: "Uri",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Artists_NormalizedName",
|
|
||||||
table: "Artists",
|
|
||||||
column: "NormalizedName",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Genres_NormalizedName",
|
|
||||||
table: "Genres",
|
|
||||||
column: "NormalizedName",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_GenreSongs_SongId",
|
|
||||||
table: "GenreSongs",
|
|
||||||
column: "SongId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Globals_Key",
|
|
||||||
table: "Globals",
|
|
||||||
column: "Key",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Images_SongId",
|
|
||||||
table: "Images",
|
|
||||||
column: "SongId",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Playlists_CreatorId",
|
|
||||||
table: "Playlists",
|
|
||||||
column: "CreatorId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Playlists_Uri",
|
|
||||||
table: "Playlists",
|
|
||||||
column: "Uri",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PlaylistSongs_SongId",
|
|
||||||
table: "PlaylistSongs",
|
|
||||||
column: "SongId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_PlaylistUsers_UserId",
|
|
||||||
table: "PlaylistUsers",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Radios_NormalizedName",
|
|
||||||
table: "Radios",
|
|
||||||
column: "NormalizedName",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Radios_UserId",
|
|
||||||
table: "Radios",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_SongInteractions_UserId",
|
|
||||||
table: "SongInteractions",
|
|
||||||
column: "UserId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Songs_AlbumId",
|
|
||||||
table: "Songs",
|
|
||||||
column: "AlbumId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Songs_ArtistId",
|
|
||||||
table: "Songs",
|
|
||||||
column: "ArtistId");
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Songs_Uri",
|
|
||||||
table: "Songs",
|
|
||||||
column: "Uri",
|
|
||||||
unique: true);
|
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
|
||||||
name: "IX_Users_Name",
|
|
||||||
table: "Users",
|
|
||||||
column: "Name",
|
|
||||||
unique: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
protected override void Down(MigrationBuilder migrationBuilder)
|
|
||||||
{
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "AlbumInteractions");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "GenreSongs");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Globals");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Images");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PlaylistSongs");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "PlaylistUsers");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Radios");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "SongInteractions");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Genres");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Playlists");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Songs");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Users");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Albums");
|
|
||||||
|
|
||||||
migrationBuilder.DropTable(
|
|
||||||
name: "Artists");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
639
Migrations/20260125172515_InitialMigration.Designer.cs
generated
Normal file
639
Migrations/20260125172515_InitialMigration.Designer.cs
generated
Normal file
@@ -0,0 +1,639 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Shadow.Data;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Shadow.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(ApplicationDbContext))]
|
||||||
|
[Migration("20260125172515_InitialMigration")]
|
||||||
|
partial class InitialMigration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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("Shadow.Entities.Album", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int?>("ArtistId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Uri")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ArtistId");
|
||||||
|
|
||||||
|
b.HasIndex("Uri")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Albums");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.AlbumInteraction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("AlbumId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("PlayDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<bool>("Starred")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("AlbumId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AlbumInteractions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Artist", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Artists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Genre", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Genres");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.GenreSong", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("GenreId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SongId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("GenreId", "SongId");
|
||||||
|
|
||||||
|
b.HasIndex("SongId");
|
||||||
|
|
||||||
|
b.ToTable("GenreSongs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Global", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Globals");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Image", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Filetype")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("SongId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Uri")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SongId")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Images");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("CreatorId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Description")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("Private")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Uri")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("CreatorId");
|
||||||
|
|
||||||
|
b.HasIndex("Uri")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Playlists");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.PlaylistSong", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("PlaylistId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SongId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Index")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("PlaylistId", "SongId");
|
||||||
|
|
||||||
|
b.HasIndex("SongId");
|
||||||
|
|
||||||
|
b.ToTable("PlaylistSongs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.PlaylistUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("PlaylistId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("PlaylistId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("PlaylistUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Radio", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Homepage")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Url")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Radios");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Song", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AlbumId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("ArtistId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int?>("BitDepth")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Bitrate")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Channels")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Comment")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("Date")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int?>("DiscNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Duration")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Filepath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Filetype")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("ImageId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Index")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("SamplingRate")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Size")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("State")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int?>("TrackNumber")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("Uri")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("AlbumId");
|
||||||
|
|
||||||
|
b.HasIndex("ArtistId");
|
||||||
|
|
||||||
|
b.HasIndex("Uri")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Songs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.SongInteraction", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("SongId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("UserId")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("PlayCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("Rating")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool?>("Starred")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.HasKey("SongId", "UserId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("SongInteractions");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Password")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("Role")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("Name")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("Users");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Album", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.Artist", "Artist")
|
||||||
|
.WithMany("Albums")
|
||||||
|
.HasForeignKey("ArtistId");
|
||||||
|
|
||||||
|
b.Navigation("Artist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.AlbumInteraction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.Album", "Album")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("AlbumId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Shadow.Entities.User", "User")
|
||||||
|
.WithMany("AlbumInteractions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Album");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.GenreSong", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.Genre", "Genre")
|
||||||
|
.WithMany("GenreSongPair")
|
||||||
|
.HasForeignKey("GenreId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Shadow.Entities.Song", "Song")
|
||||||
|
.WithMany("GenreSongPair")
|
||||||
|
.HasForeignKey("SongId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Genre");
|
||||||
|
|
||||||
|
b.Navigation("Song");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Image", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.Song", "Song")
|
||||||
|
.WithOne("Image")
|
||||||
|
.HasForeignKey("Shadow.Entities.Image", "SongId");
|
||||||
|
|
||||||
|
b.Navigation("Song");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.User", "Creator")
|
||||||
|
.WithMany("Playlists")
|
||||||
|
.HasForeignKey("CreatorId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Creator");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.PlaylistSong", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.Playlist", "Playlist")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("PlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Shadow.Entities.Song", "Song")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SongId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Playlist");
|
||||||
|
|
||||||
|
b.Navigation("Song");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.PlaylistUser", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.Playlist", "Playlist")
|
||||||
|
.WithMany("AuthorizedPlaylistUsers")
|
||||||
|
.HasForeignKey("PlaylistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Shadow.Entities.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Playlist");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Radio", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.User", "User")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Song", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.Album", "Album")
|
||||||
|
.WithMany("Songs")
|
||||||
|
.HasForeignKey("AlbumId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Shadow.Entities.Artist", "Artist")
|
||||||
|
.WithMany("Songs")
|
||||||
|
.HasForeignKey("ArtistId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Album");
|
||||||
|
|
||||||
|
b.Navigation("Artist");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.SongInteraction", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Shadow.Entities.Song", "Song")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SongId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("Shadow.Entities.User", "User")
|
||||||
|
.WithMany("SongInteractions")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Song");
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Album", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Songs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Artist", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Albums");
|
||||||
|
|
||||||
|
b.Navigation("Songs");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Genre", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("GenreSongPair");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Playlist", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AuthorizedPlaylistUsers");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.Song", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("GenreSongPair");
|
||||||
|
|
||||||
|
b.Navigation("Image");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Shadow.Entities.User", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("AlbumInteractions");
|
||||||
|
|
||||||
|
b.Navigation("Playlists");
|
||||||
|
|
||||||
|
b.Navigation("SongInteractions");
|
||||||
|
});
|
||||||
|
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
483
Migrations/20260125172515_InitialMigration.cs
Normal file
483
Migrations/20260125172515_InitialMigration.cs
Normal file
@@ -0,0 +1,483 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Shadow.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class InitialMigration : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Artists",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
NormalizedName = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Artists", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Genres",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
NormalizedName = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Genres", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Globals",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Key = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Value = table.Column<string>(type: "text", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Globals", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
NormalizedName = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Password = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Role = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Albums",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Uri = table.Column<string>(type: "text", nullable: false),
|
||||||
|
State = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ArtistId = table.Column<int>(type: "integer", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Albums", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Albums_Artists_ArtistId",
|
||||||
|
column: x => x.ArtistId,
|
||||||
|
principalTable: "Artists",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Playlists",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Uri = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Description = table.Column<string>(type: "text", nullable: false),
|
||||||
|
CreatorId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Private = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Playlists", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Playlists_Users_CreatorId",
|
||||||
|
column: x => x.CreatorId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Radios",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Name = table.Column<string>(type: "text", nullable: false),
|
||||||
|
NormalizedName = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Homepage = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Url = table.Column<string>(type: "text", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Radios", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Radios_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "AlbumInteractions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
AlbumId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
PlayDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: true),
|
||||||
|
Starred = table.Column<bool>(type: "boolean", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_AlbumInteractions", x => new { x.AlbumId, x.UserId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AlbumInteractions_Albums_AlbumId",
|
||||||
|
column: x => x.AlbumId,
|
||||||
|
principalTable: "Albums",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_AlbumInteractions_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Songs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Title = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Uri = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Filepath = table.Column<string>(type: "text", nullable: false),
|
||||||
|
State = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Filetype = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Date = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
Duration = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Bitrate = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Size = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Comment = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Channels = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SamplingRate = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
BitDepth = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
Index = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
TrackNumber = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
DiscNumber = table.Column<int>(type: "integer", nullable: true),
|
||||||
|
AlbumId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ArtistId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
ImageId = table.Column<int>(type: "integer", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Songs", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Songs_Albums_AlbumId",
|
||||||
|
column: x => x.AlbumId,
|
||||||
|
principalTable: "Albums",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Songs_Artists_ArtistId",
|
||||||
|
column: x => x.ArtistId,
|
||||||
|
principalTable: "Artists",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PlaylistUsers",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
PlaylistId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PlaylistUsers", x => new { x.PlaylistId, x.UserId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlaylistUsers_Playlists_PlaylistId",
|
||||||
|
column: x => x.PlaylistId,
|
||||||
|
principalTable: "Playlists",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlaylistUsers_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "GenreSongs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
GenreId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SongId = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_GenreSongs", x => new { x.GenreId, x.SongId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_GenreSongs_Genres_GenreId",
|
||||||
|
column: x => x.GenreId,
|
||||||
|
principalTable: "Genres",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_GenreSongs_Songs_SongId",
|
||||||
|
column: x => x.SongId,
|
||||||
|
principalTable: "Songs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Images",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||||
|
Uri = table.Column<string>(type: "text", nullable: false),
|
||||||
|
Filetype = table.Column<string>(type: "text", nullable: false),
|
||||||
|
State = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SongId = table.Column<int>(type: "integer", nullable: true)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Images", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_Images_Songs_SongId",
|
||||||
|
column: x => x.SongId,
|
||||||
|
principalTable: "Songs",
|
||||||
|
principalColumn: "Id");
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "PlaylistSongs",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
PlaylistId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
SongId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Index = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_PlaylistSongs", x => new { x.PlaylistId, x.SongId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlaylistSongs_Playlists_PlaylistId",
|
||||||
|
column: x => x.PlaylistId,
|
||||||
|
principalTable: "Playlists",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_PlaylistSongs_Songs_SongId",
|
||||||
|
column: x => x.SongId,
|
||||||
|
principalTable: "Songs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "SongInteractions",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
SongId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
UserId = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Id = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
PlayCount = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Starred = table.Column<bool>(type: "boolean", nullable: true),
|
||||||
|
Rating = table.Column<int>(type: "integer", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_SongInteractions", x => new { x.SongId, x.UserId });
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SongInteractions_Songs_SongId",
|
||||||
|
column: x => x.SongId,
|
||||||
|
principalTable: "Songs",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_SongInteractions_Users_UserId",
|
||||||
|
column: x => x.UserId,
|
||||||
|
principalTable: "Users",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_AlbumInteractions_UserId",
|
||||||
|
table: "AlbumInteractions",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Albums_ArtistId",
|
||||||
|
table: "Albums",
|
||||||
|
column: "ArtistId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Albums_Uri",
|
||||||
|
table: "Albums",
|
||||||
|
column: "Uri",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Artists_NormalizedName",
|
||||||
|
table: "Artists",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Genres_NormalizedName",
|
||||||
|
table: "Genres",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_GenreSongs_SongId",
|
||||||
|
table: "GenreSongs",
|
||||||
|
column: "SongId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Globals_Key",
|
||||||
|
table: "Globals",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Images_SongId",
|
||||||
|
table: "Images",
|
||||||
|
column: "SongId",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Playlists_CreatorId",
|
||||||
|
table: "Playlists",
|
||||||
|
column: "CreatorId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Playlists_Uri",
|
||||||
|
table: "Playlists",
|
||||||
|
column: "Uri",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlaylistSongs_SongId",
|
||||||
|
table: "PlaylistSongs",
|
||||||
|
column: "SongId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_PlaylistUsers_UserId",
|
||||||
|
table: "PlaylistUsers",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Radios_NormalizedName",
|
||||||
|
table: "Radios",
|
||||||
|
column: "NormalizedName",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Radios_UserId",
|
||||||
|
table: "Radios",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_SongInteractions_UserId",
|
||||||
|
table: "SongInteractions",
|
||||||
|
column: "UserId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Songs_AlbumId",
|
||||||
|
table: "Songs",
|
||||||
|
column: "AlbumId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Songs_ArtistId",
|
||||||
|
table: "Songs",
|
||||||
|
column: "ArtistId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Songs_Uri",
|
||||||
|
table: "Songs",
|
||||||
|
column: "Uri",
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Users_Name",
|
||||||
|
table: "Users",
|
||||||
|
column: "Name",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "AlbumInteractions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "GenreSongs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Globals");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Images");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PlaylistSongs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "PlaylistUsers");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Radios");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "SongInteractions");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Genres");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Playlists");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Songs");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Albums");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Artists");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
// <auto-generated />
|
// <auto-generated />
|
||||||
using System;
|
using System;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
@@ -213,6 +213,9 @@ namespace Shadow.Migrations
|
|||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("Private")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<string>("Uri")
|
b.Property<string>("Uri")
|
||||||
.IsRequired()
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Microsoft.OpenApi;
|
|||||||
using Shadow.Data;
|
using Shadow.Data;
|
||||||
using Shadow.Tools;
|
using Shadow.Tools;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Xml;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
@@ -17,7 +18,10 @@ builder.Services.AddDbContext<ApplicationDbContext>(options =>
|
|||||||
builder.Services.AddControllers(options =>
|
builder.Services.AddControllers(options =>
|
||||||
{
|
{
|
||||||
// Add XML serializer
|
// Add XML serializer
|
||||||
options.OutputFormatters.Add(new XmlSerializerOutputFormatter());
|
// options.RespectBrowserAcceptHeader = true;
|
||||||
|
// options.OutputFormatters.Add(new XmlSerializerOutputFormatter(
|
||||||
|
// new XmlWriterSettings { OmitXmlDeclaration = true }
|
||||||
|
// ));
|
||||||
}).AddJsonOptions(options =>
|
}).AddJsonOptions(options =>
|
||||||
{
|
{
|
||||||
// Pretty-print JSON
|
// Pretty-print JSON
|
||||||
@@ -70,6 +74,7 @@ builder.Services.AddHttpLogging();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
bool shutdown = false;
|
bool shutdown = false;
|
||||||
|
bool scanNeeded = false;
|
||||||
if (args.FirstOrDefault() is not null)
|
if (args.FirstOrDefault() is not null)
|
||||||
{
|
{
|
||||||
// Handle CLI if arguments have been passed.
|
// Handle CLI if arguments have been passed.
|
||||||
@@ -89,6 +94,7 @@ using (IServiceScope scope = app.Services.CreateScope())
|
|||||||
GeneralUseHelpers guhf = scope.ServiceProvider.GetRequiredService<GeneralUseHelpers>();
|
GeneralUseHelpers guhf = scope.ServiceProvider.GetRequiredService<GeneralUseHelpers>();
|
||||||
Seeder seeder = new(db, guhf);
|
Seeder seeder = new(db, guhf);
|
||||||
shutdown = seeder.Seed();
|
shutdown = seeder.Seed();
|
||||||
|
if (!shutdown) scanNeeded = await seeder.ScanPrefetchAsync();
|
||||||
}
|
}
|
||||||
if (shutdown) return;
|
if (shutdown) return;
|
||||||
|
|
||||||
|
|||||||
@@ -43,11 +43,4 @@
|
|||||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.1" />
|
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerUI" Version="10.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Folder Include="Controllers\" />
|
|
||||||
<Folder Include="Migrations\" />
|
|
||||||
<Folder Include="Mapping\" />
|
|
||||||
<Folder Include="DTOs\" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
151
Tools/Cli.cs
151
Tools/Cli.cs
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Shadow.Data;
|
using Shadow.Data;
|
||||||
using Shadow.Entities;
|
using Shadow.Entities;
|
||||||
|
|
||||||
@@ -8,7 +9,8 @@ public class Cli
|
|||||||
private readonly GeneralUseHelpers guhf;
|
private readonly GeneralUseHelpers guhf;
|
||||||
private readonly string[] args;
|
private readonly string[] args;
|
||||||
|
|
||||||
// TODO: Add "changeUser", "fullRescan", "destructiveRescan"
|
// TODO: Add "changeUser", "fullRescan", "destructiveRescan", "healthcheck"
|
||||||
|
// TODO: Make this async
|
||||||
|
|
||||||
public Cli(ApplicationDbContext _db, GeneralUseHelpers _guhf, string[] _args)
|
public Cli(ApplicationDbContext _db, GeneralUseHelpers _guhf, string[] _args)
|
||||||
{
|
{
|
||||||
@@ -65,6 +67,15 @@ public class Cli
|
|||||||
if (!Seeder.EnsureMigrations(db)) return true;
|
if (!Seeder.EnsureMigrations(db)) return true;
|
||||||
SetupWizard();
|
SetupWizard();
|
||||||
break;
|
break;
|
||||||
|
case "clear":
|
||||||
|
case "cleanup":
|
||||||
|
case "cleardb":
|
||||||
|
case "cleanupdb":
|
||||||
|
case "--clear":
|
||||||
|
case "--cleanup":
|
||||||
|
if (!Seeder.EnsureMigrations(db)) return true;
|
||||||
|
CleanUpDb();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Console.WriteLine($"Unknown option: \"{args[0]}\". See \"help\" for available arguments.");
|
Console.WriteLine($"Unknown option: \"{args[0]}\". See \"help\" for available arguments.");
|
||||||
break;
|
break;
|
||||||
@@ -88,9 +99,10 @@ public class Cli
|
|||||||
$"- removeUser [username] - remove the user COMPLETELY.\n" +
|
$"- removeUser [username] - remove the user COMPLETELY.\n" +
|
||||||
$"\n" +
|
$"\n" +
|
||||||
$"=== Server maintenance ===\n" +
|
$"=== Server maintenance ===\n" +
|
||||||
$"- setupWizard - configure library and thumbnail location on disk.\n" +
|
$"- setupWizard - configure library and thumbnail location on disk,\n" +
|
||||||
|
$"- cleanup - cleanup database from orphaned songs/albums/artists.\n" +
|
||||||
$"\n" +
|
$"\n" +
|
||||||
$"Username is optional. If not provided, user will be prompted to enter it.\n" +
|
$"[field] means the field is optional. If not provided, user will be prompted to enter it.\n" +
|
||||||
$"Running without specifying a command launches a Kestrel web server.\n" +
|
$"Running without specifying a command launches a Kestrel web server.\n" +
|
||||||
$"\n" +
|
$"\n" +
|
||||||
$"License: AGPLv3+, Source code: https://gitea.7o7.cx/sherl/Shadow"
|
$"License: AGPLv3+, Source code: https://gitea.7o7.cx/sherl/Shadow"
|
||||||
@@ -142,7 +154,8 @@ public class Cli
|
|||||||
{
|
{
|
||||||
Name = username!,
|
Name = username!,
|
||||||
NormalizedName = username!.ToLower(),
|
NormalizedName = username!.ToLower(),
|
||||||
Password = password
|
Password = password,
|
||||||
|
Role = isUserAdmin ? 0 : 1 // 0 = admin
|
||||||
};
|
};
|
||||||
|
|
||||||
db.Users.Add(newUser);
|
db.Users.Add(newUser);
|
||||||
@@ -206,9 +219,9 @@ public class Cli
|
|||||||
|
|
||||||
Console.WriteLine($"[Shadow] Remove user");
|
Console.WriteLine($"[Shadow] Remove user");
|
||||||
if (args.Length == 2)
|
if (args.Length == 2)
|
||||||
Console.WriteLine($" You will be promped to enter the password.");
|
Console.WriteLine($" You will be prompted to enter the password.");
|
||||||
else
|
else
|
||||||
Console.WriteLine($" You will be promped to enter the username and password.");
|
Console.WriteLine($" You will be prompted to enter the username and password.");
|
||||||
|
|
||||||
string? username = null;
|
string? username = null;
|
||||||
if (args.Length != 2)
|
if (args.Length != 2)
|
||||||
@@ -281,6 +294,8 @@ public class Cli
|
|||||||
" - Shadow's image thumbnails path, used to provide cover art extracted from media.\n" +
|
" - Shadow's image thumbnails path, used to provide cover art extracted from media.\n" +
|
||||||
" Finally, you will be asked if you want your changes saved.\n");
|
" Finally, you will be asked if you want your changes saved.\n");
|
||||||
|
|
||||||
|
// TODO: do a healthcheck here
|
||||||
|
|
||||||
Global? musicLibraryPath = db.Globals.FirstOrDefault(g => g.Key == "musicLibraryPath");
|
Global? musicLibraryPath = db.Globals.FirstOrDefault(g => g.Key == "musicLibraryPath");
|
||||||
Global? musicThumbnailPath = db.Globals.FirstOrDefault(g => g.Key == "musicThumbnailPath");
|
Global? musicThumbnailPath = db.Globals.FirstOrDefault(g => g.Key == "musicThumbnailPath");
|
||||||
|
|
||||||
@@ -299,7 +314,7 @@ public class Cli
|
|||||||
Console.WriteLine(" No existing configuration found.\n");
|
Console.WriteLine(" No existing configuration found.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
string newMusicLibraryPath = DefaultPrompt("Please enter your new music library path", musicLibraryPath?.Value).TrimEnd(['\\', '/']);
|
string newMusicLibraryPath = DefaultPrompt("Please enter your new music library path", musicLibraryPath?.Value).TrimEnd(['\\', '/']);
|
||||||
string newMusicThumbnailPath = DefaultPrompt("Please enter your new image thumbnail path", musicThumbnailPath?.Value ?? Path.Combine(newMusicLibraryPath, ".shadow")).TrimEnd(['\\', '/']);
|
string newMusicThumbnailPath = DefaultPrompt("Please enter your new image thumbnail path", musicThumbnailPath?.Value ?? Path.Combine(newMusicLibraryPath, ".shadow")).TrimEnd(['\\', '/']);
|
||||||
|
|
||||||
bool confirmed = YesNoPrompt("Are you sure you want:\n" +
|
bool confirmed = YesNoPrompt("Are you sure you want:\n" +
|
||||||
@@ -322,6 +337,32 @@ public class Cli
|
|||||||
musicLibraryPath.Value = newMusicLibraryPath;
|
musicLibraryPath.Value = newMusicLibraryPath;
|
||||||
musicThumbnailPath.Value = newMusicThumbnailPath;
|
musicThumbnailPath.Value = newMusicThumbnailPath;
|
||||||
|
|
||||||
|
System.IO.Directory.CreateDirectory(newMusicLibraryPath);
|
||||||
|
System.IO.Directory.CreateDirectory(newMusicThumbnailPath);
|
||||||
|
|
||||||
|
// Try to find the Assets directory automatically
|
||||||
|
string currentPath = AppContext.BaseDirectory;
|
||||||
|
for (int i = 0; i < 5; i++)
|
||||||
|
{
|
||||||
|
if (Directory.GetFiles(currentPath, "Shadow.slnx")
|
||||||
|
.FirstOrDefault() != null)
|
||||||
|
{
|
||||||
|
File.Copy(
|
||||||
|
Path.Combine(currentPath, "Assets", "vinyl.png"),
|
||||||
|
Path.Combine(newMusicThumbnailPath, "default.png"),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentPath = Path.Combine(currentPath, "..");
|
||||||
|
|
||||||
|
if (i == 5) Console.WriteLine("\n" +
|
||||||
|
"[Error] Could not determine content root path. \n" +
|
||||||
|
"Please place Assets/vinyl.png in your thumbnail path directory manually\n" +
|
||||||
|
"and rename it to default.png.");
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateRange can both Add and Update rows.
|
// UpdateRange can both Add and Update rows.
|
||||||
db.Globals.UpdateRange(musicLibraryPath, musicThumbnailPath);
|
db.Globals.UpdateRange(musicLibraryPath, musicThumbnailPath);
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
@@ -338,6 +379,102 @@ public class Cli
|
|||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CleanUpDb()
|
||||||
|
{
|
||||||
|
// Ask user here whether he/she wants to
|
||||||
|
// clear the DB from dangling records (orphaned songs/albums/artists).
|
||||||
|
int rowsAffected = 0;
|
||||||
|
|
||||||
|
// Retrieve orphaned songs
|
||||||
|
List<Song> orphanedSongs = db.Songs
|
||||||
|
.Where(s => s.State == 1)
|
||||||
|
.Include(s => s.Album)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Ask if it's alright to remove them
|
||||||
|
// and related listening data permanently
|
||||||
|
if (orphanedSongs.Count != 0)
|
||||||
|
{
|
||||||
|
bool songConsent = false;
|
||||||
|
songConsent = YesNoPrompt($"Found {orphanedSongs.Count} orphaned songs. Remove them? [y/N]: ", false);
|
||||||
|
if (songConsent)
|
||||||
|
{
|
||||||
|
foreach (Song s in orphanedSongs)
|
||||||
|
{
|
||||||
|
// Remove song interactions
|
||||||
|
List<SongInteraction> interactions = db.SongInteractions.Where(si => si.Song == s).ToList();
|
||||||
|
db.SongInteractions.RemoveRange(interactions);
|
||||||
|
|
||||||
|
// Remove song from playlists
|
||||||
|
List<PlaylistSong> playlists = db.PlaylistSongs.Where(ps => ps.Song == s).ToList();
|
||||||
|
db.PlaylistSongs.RemoveRange(playlists);
|
||||||
|
|
||||||
|
// Remove song from albums
|
||||||
|
Album album = s.Album;
|
||||||
|
album.Songs.Remove(s);
|
||||||
|
|
||||||
|
if (album.Songs.Count == 0) album.State = 1; // Set album state to orphaned.
|
||||||
|
|
||||||
|
// TODO: Remove song images if not used by any other resource
|
||||||
|
// ...
|
||||||
|
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform cleanup with stored procedure
|
||||||
|
db.Database.ExecuteSqlRaw("CALL song_cleanup()");
|
||||||
|
rowsAffected += orphanedSongs.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else Console.WriteLine("No orphaned songs found.");
|
||||||
|
|
||||||
|
// Rinse and repeat
|
||||||
|
// Retrieve orphaned albums
|
||||||
|
List<Album> orphanedAlbums = db.Albums.Where(a => a.State == 1).ToList();
|
||||||
|
|
||||||
|
if (orphanedAlbums.Count != 0)
|
||||||
|
{
|
||||||
|
bool albumConsent = false;
|
||||||
|
albumConsent = YesNoPrompt($"Found {orphanedAlbums.Count} orphaned albums. Remove them? [y/N]: ", false);
|
||||||
|
if (albumConsent)
|
||||||
|
{
|
||||||
|
db.Albums.RemoveRange(orphanedAlbums);
|
||||||
|
rowsAffected += orphanedAlbums.Count;
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else Console.WriteLine("No orphaned albums found.");
|
||||||
|
|
||||||
|
// Retrieve orphaned artists (artists with no songs AND albums)
|
||||||
|
List<Artist> orphanedArtists = db.Artists
|
||||||
|
.Where(a => a.Songs.Count == 0 && a.Albums.Count == 0)
|
||||||
|
.ToList();
|
||||||
|
Artist? unknownArtist = db.Artists
|
||||||
|
.FirstOrDefault(a => a.NormalizedName == "[unknown artist]");
|
||||||
|
|
||||||
|
// Account for the [Unknown Artist],
|
||||||
|
// which is a meta-artist and shall
|
||||||
|
// not be slated for removal.
|
||||||
|
// Can be null only if this command
|
||||||
|
// is ran before seeding.
|
||||||
|
if (unknownArtist != null) orphanedArtists.Remove(unknownArtist);
|
||||||
|
|
||||||
|
if (orphanedArtists.Count != 0)
|
||||||
|
{
|
||||||
|
bool artistConsent = false;
|
||||||
|
artistConsent = YesNoPrompt($"Found {orphanedArtists.Count} orphaned artists. Remove them? [y/N]: ", false);
|
||||||
|
if (artistConsent)
|
||||||
|
{
|
||||||
|
db.Artists.RemoveRange(orphanedArtists);
|
||||||
|
rowsAffected += orphanedArtists.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else Console.WriteLine("No orphaned artists found.");
|
||||||
|
|
||||||
|
Console.WriteLine($"{rowsAffected} entries affected.");
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
public static string ReadPassword(string prompt = " Enter password (will not be echoed back): ")
|
public static string ReadPassword(string prompt = " Enter password (will not be echoed back): ")
|
||||||
{
|
{
|
||||||
// https://www.silicloud.com/blog/how-to-hide-content-in-the-console-using-c/
|
// https://www.silicloud.com/blog/how-to-hide-content-in-the-console-using-c/
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Shadow.Data;
|
using Shadow.Data;
|
||||||
using Shadow.Entities;
|
using Shadow.Entities;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Xml.Serialization;
|
||||||
|
|
||||||
namespace Shadow.Tools;
|
namespace Shadow.Tools;
|
||||||
|
|
||||||
public class GeneralUseHelpers(ApplicationDbContext db, IConfiguration appsettings)
|
public class GeneralUseHelpers(ApplicationDbContext? db = null, IConfiguration? appsettings = null)
|
||||||
{
|
{
|
||||||
private readonly ApplicationDbContext _db = db;
|
private readonly ApplicationDbContext? _db = db;
|
||||||
private readonly IConfiguration _appsettings = appsettings;
|
private readonly IConfiguration? _appsettings = appsettings;
|
||||||
|
|
||||||
|
|
||||||
//async public Task<User?> GetUserFromEmail(string email)
|
//async public Task<User?> GetUserFromEmail(string email)
|
||||||
@@ -27,4 +28,74 @@ public class GeneralUseHelpers(ApplicationDbContext db, IConfiguration appsettin
|
|||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Quick and dirty Dictionary<string, string> to JSON serializer
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="dict">Dictionary with keypair of two strings</param>
|
||||||
|
/// <returns>Minified JSON</returns>
|
||||||
|
public static string DictAsJson(Dictionary<string, string> dict)
|
||||||
|
{
|
||||||
|
string resultJson = String.Empty;
|
||||||
|
|
||||||
|
foreach (string key in dict.Keys)
|
||||||
|
{
|
||||||
|
string cleanKey = key.Replace("\"", "\\\""); // "a"b" -> "a\"b"
|
||||||
|
string cleanValue = dict[key].Replace("\"", "\\\"");
|
||||||
|
resultJson += $"\"{cleanKey}\": \"{cleanValue}\", " // "key": "val",<space>
|
||||||
|
.Replace(@"\", @"\\"); // a\b -> a\\b
|
||||||
|
}
|
||||||
|
|
||||||
|
return "{" + resultJson[..^2] + "}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> GetUserFromForm(HttpRequest request)
|
||||||
|
{
|
||||||
|
User? user = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string username = request.Form["u"]!;
|
||||||
|
string saltedPassword = request.Form["t"]!;
|
||||||
|
string sesame = request.Form["s"]!;
|
||||||
|
|
||||||
|
User? foundUser = _db?.Users
|
||||||
|
.FirstOrDefault(u => u.NormalizedName == username.ToLower());
|
||||||
|
|
||||||
|
if (foundUser == null)
|
||||||
|
return user;
|
||||||
|
|
||||||
|
string resaltedPassword = MetadataExtractor.GetStringMD5($"{foundUser.Password}{sesame}");
|
||||||
|
if (resaltedPassword == saltedPassword)
|
||||||
|
user = foundUser;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
user = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User?> GetUserFromParams(HttpRequest request)
|
||||||
|
{
|
||||||
|
User? user = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string username = request.Query["u"]!;
|
||||||
|
string saltedPassword = request.Query["t"]!;
|
||||||
|
string sesame = request.Query["s"]!;
|
||||||
|
|
||||||
|
User foundUser = _db!.Users
|
||||||
|
.FirstOrDefault(u => u.NormalizedName == username.ToLower())!;
|
||||||
|
|
||||||
|
string resaltedPassword = MetadataExtractor.GetStringMD5($"{foundUser.Password}{sesame}");
|
||||||
|
if (resaltedPassword == saltedPassword)
|
||||||
|
user = foundUser;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
user = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
144
Tools/LibraryWatcher.cs
Normal file
144
Tools/LibraryWatcher.cs
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Shadow.Data;
|
||||||
|
using Shadow.Entities;
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Shadow.Tools;
|
||||||
|
public class LibraryWatcher(string watchPath, string[] excludedPaths, ApplicationDbContext dbContext)
|
||||||
|
{
|
||||||
|
private readonly string libraryPath = watchPath;
|
||||||
|
private readonly string[] excludedPaths = excludedPaths;
|
||||||
|
private readonly ApplicationDbContext db = dbContext;
|
||||||
|
private readonly GeneralUseHelpers guhf = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a sorted list of paths to all files in a directory, recursively.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="directory">Path to directory</param>
|
||||||
|
/// <returns>Sorted list of filepaths</returns>
|
||||||
|
public async Task<List<string>> GetFilesRecursivelyAsync(string directory)
|
||||||
|
{
|
||||||
|
string[] allowedExtensions = [".flac", ".m4a", ".mp3", ".ogg", ".wav"];
|
||||||
|
try
|
||||||
|
{
|
||||||
|
List<string> files =
|
||||||
|
Directory.GetFiles(directory, "*", SearchOption.AllDirectories)
|
||||||
|
.Where(file => allowedExtensions.Any(file.ToLower().EndsWith))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
files.Sort();
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
catch (DirectoryNotFoundException)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Error] Directory \"{directory}\" does not exist!\n" +
|
||||||
|
" Please create it manually, or use `Shadow setupWizard`.");
|
||||||
|
throw new DirectoryNotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return all multimedia content inside of library
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>List of multimedia filepaths</returns>
|
||||||
|
public async Task<List<string>> GetAllMultimediaAsync()
|
||||||
|
{
|
||||||
|
|
||||||
|
// List files in cache
|
||||||
|
// Note: currently, the only excluded path from scanning is the thumbnail cache.
|
||||||
|
// This might change in the future.
|
||||||
|
List<string> cacheFiles = await GetFilesRecursivelyAsync(excludedPaths[0]);
|
||||||
|
|
||||||
|
// List files in library excluding cache
|
||||||
|
List<string> libraryContent = await GetFilesRecursivelyAsync(libraryPath);
|
||||||
|
List<string> libraryMultimedia = libraryContent.Except(cacheFiles).ToList();
|
||||||
|
|
||||||
|
return libraryMultimedia;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scan the library in its entirety
|
||||||
|
/// </summary>
|
||||||
|
/// <returns></returns>
|
||||||
|
public async Task<List<string>> PerformFullScanAsync()
|
||||||
|
{
|
||||||
|
Console.WriteLine("Performing full library scan...");
|
||||||
|
|
||||||
|
// Current library state as present in database
|
||||||
|
List<Song> currentLibraryMedia = await db.Songs
|
||||||
|
.Where(s => s.State == 0)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
// Updated library state
|
||||||
|
List<Song> updatedSongList = [];
|
||||||
|
List<string> newMultimediaPathNames = await GetAllMultimediaAsync();
|
||||||
|
foreach (string filepath in newMultimediaPathNames)
|
||||||
|
{
|
||||||
|
Console.WriteLine(filepath);
|
||||||
|
Dictionary<string, string> fileInfo = await MetadataExtractor.ExtractAsync(filepath);
|
||||||
|
|
||||||
|
// Pretend we are doing parsing here...
|
||||||
|
Console.WriteLine(GeneralUseHelpers.DictAsJson(fileInfo));
|
||||||
|
Song? songInDb = db.Songs
|
||||||
|
.Include(s => s.Album)
|
||||||
|
.FirstOrDefault(s => s.Uri == fileInfo["_shadow:fileHash"]);
|
||||||
|
if (songInDb != null)
|
||||||
|
{
|
||||||
|
// Don't parse the song
|
||||||
|
Console.WriteLine("Skipping song as it already exists in database...");
|
||||||
|
|
||||||
|
// But update it's location in case it has been moved
|
||||||
|
songInDb.Filepath = filepath;
|
||||||
|
|
||||||
|
// And state in case it has been reinstated
|
||||||
|
songInDb.State = 0; // Set non-orphaned state
|
||||||
|
songInDb.Album.State = 0; // -||-
|
||||||
|
db.Update(songInDb); // Is this necessary?
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// Afterwards include it in the updated song list
|
||||||
|
updatedSongList.Add(songInDb);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// A new song? Parse it and add to DB
|
||||||
|
Song? newSong = MediaParser.CreateSong(db, fileInfo);
|
||||||
|
|
||||||
|
// Sanity check
|
||||||
|
if (newSong != null)
|
||||||
|
updatedSongList.Add(newSong);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine($"Full scan complete! Processed {newMultimediaPathNames.Count} files.");
|
||||||
|
|
||||||
|
List<Song> orphanedSongs = currentLibraryMedia
|
||||||
|
.Except(updatedSongList)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Console.WriteLine($"Detected {orphanedSongs.Count} new orphaned songs");
|
||||||
|
foreach (Song s in orphanedSongs)
|
||||||
|
{
|
||||||
|
Song dbSong = db.Songs
|
||||||
|
.Include(d => d.Artist)
|
||||||
|
.First(d => d.Id == s.Id);
|
||||||
|
Console.WriteLine($"- {dbSong.Title} by {dbSong.Artist.Name} (previous path: {dbSong.Filepath})");
|
||||||
|
dbSong.State = 1;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine();
|
||||||
|
|
||||||
|
// Update state inside of DB
|
||||||
|
string updatedLibraryState = MetadataExtractor.GetStringMD5(string.Join("\n", newMultimediaPathNames));
|
||||||
|
Global lastLibraryState = db.Globals.FirstOrDefault(g => g.Key == "libraryState")
|
||||||
|
?? new() { Key = "libraryState"};
|
||||||
|
lastLibraryState.Value = updatedLibraryState;
|
||||||
|
db.Update(lastLibraryState);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
return newMultimediaPathNames;
|
||||||
|
}
|
||||||
|
}
|
||||||
129
Tools/MediaParser.cs
Normal file
129
Tools/MediaParser.cs
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
using Microsoft.VisualBasic.FileIO;
|
||||||
|
using Shadow.Data;
|
||||||
|
using Shadow.Entities;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
|
||||||
|
namespace Shadow.Tools;
|
||||||
|
|
||||||
|
public static class MediaParser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Generate a random hex string (with length 32 by default)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="length">Optional: hexstring length</param>
|
||||||
|
/// <returns>A hexstring of given length</returns>
|
||||||
|
public static string HexStr(int length = 32)
|
||||||
|
{
|
||||||
|
return RandomNumberGenerator.GetHexString(length).ToLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get metadata content opportunistically
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="metadata">Dictionary to search in</param>
|
||||||
|
/// <param name="searchStrings">Keywords to search for</param>
|
||||||
|
/// <returns>Retrieved value (string) on success, otherwise null</returns>
|
||||||
|
public static string? GetAny(Dictionary<string, string> metadata, List<string> searchStrings)
|
||||||
|
{
|
||||||
|
foreach (string searchString in searchStrings)
|
||||||
|
{
|
||||||
|
if (metadata.TryGetValue(searchString, out string? value) && !string.IsNullOrEmpty(value))
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map exiftool metadata to Song entity.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="db">Database context</param>
|
||||||
|
/// <param name="exif">ExifTool metadata</param>
|
||||||
|
/// <returns>New Song entity, or null if song already exists in db</returns>
|
||||||
|
public static Song? CreateSong(ApplicationDbContext db, Dictionary<string, string> exif)
|
||||||
|
{
|
||||||
|
|
||||||
|
// First of all, check if song already exists in db
|
||||||
|
string uri = GetAny(exif, ["_shadow:fileHash"])
|
||||||
|
?? throw new Exception("Fatal error: could not get file hash!");
|
||||||
|
if (db.Songs.FirstOrDefault(s => s.Uri == uri) != null) return null;
|
||||||
|
|
||||||
|
// If not, extract exif data
|
||||||
|
string title = GetAny(exif, [
|
||||||
|
"ItemList:Title", // iTunes m4a
|
||||||
|
"ItemList:SortName", // iTunes m4a
|
||||||
|
"Vorbis:Title", // Bandcamp ogg
|
||||||
|
"ID3v2_3:Title", // Generic mp3/wav ID3 v2.3.0
|
||||||
|
])
|
||||||
|
?? Path.Combine(exif["System:Directory"], exif["System:FileName"]);
|
||||||
|
string filepath = Path.GetFullPath(
|
||||||
|
Path.Combine(exif["System:Directory"], exif["System:FileName"])
|
||||||
|
); // TODO: bulletproof this
|
||||||
|
string filetype = exif["File:FileType"].ToLower();
|
||||||
|
|
||||||
|
// Album/artist related
|
||||||
|
string artistName = GetAny(exif, [
|
||||||
|
"ItemList:Artist", // iTunes m4a
|
||||||
|
"ItemList:AlbumArtist", // iTunes m4a
|
||||||
|
"Vorbis:Artist", // Bandcamp m4a
|
||||||
|
"Vorbis:Albumartist", // Bandcamp m4a
|
||||||
|
"ID3v2_3:Artist", // Generic mp3/wav ID3 v2.3.0
|
||||||
|
]) ?? "[Unknown Artist]"; // this is a weak line of defense against deliberately crafted
|
||||||
|
|
||||||
|
string albumName = GetAny(exif, [
|
||||||
|
"ItemList:Album", // iTunes m4a
|
||||||
|
"Vorbis:Album", // Bandcamp m4a
|
||||||
|
"ID3v2_3:Album", // Generic mp3/wav ID3 v2.3.0
|
||||||
|
]) ?? "[Unknown Album]"; // again, weak line of defense
|
||||||
|
|
||||||
|
// TODO: Try and find genres
|
||||||
|
|
||||||
|
// Try to find relevant artists and albums
|
||||||
|
Artist artist = db.Artists
|
||||||
|
.FirstOrDefault(a => a.NormalizedName == artistName.ToLower())
|
||||||
|
?? new Artist
|
||||||
|
{
|
||||||
|
Name = artistName,
|
||||||
|
NormalizedName = artistName.ToLower()
|
||||||
|
};
|
||||||
|
|
||||||
|
Album album = db.Albums
|
||||||
|
.FirstOrDefault(a => a.Name == albumName && a.Artist == artist)
|
||||||
|
?? new Album
|
||||||
|
{
|
||||||
|
Name = albumName,
|
||||||
|
Uri = HexStr(),
|
||||||
|
Artist = artist
|
||||||
|
};
|
||||||
|
|
||||||
|
Song song = new()
|
||||||
|
{
|
||||||
|
Title = title,
|
||||||
|
Uri = uri,
|
||||||
|
Filepath = filepath,
|
||||||
|
Filetype = filetype,
|
||||||
|
Album = album,
|
||||||
|
Artist = artist
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Is Update() safe here?
|
||||||
|
db.Artists.Update(artist);
|
||||||
|
db.Albums.Update(album);
|
||||||
|
db.SaveChanges();
|
||||||
|
db.Songs.Update(song);
|
||||||
|
artist.Albums.Add(album);
|
||||||
|
artist.Songs.Add(song);
|
||||||
|
db.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[Error: MediaParser] Failed to extract metadata from {filepath}:\n" +
|
||||||
|
$"{e}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return song;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
67
Tools/MetadataExtractor.cs
Normal file
67
Tools/MetadataExtractor.cs
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
using SharpExifTool;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
namespace Shadow.Tools;
|
||||||
|
|
||||||
|
public static class MetadataExtractor
|
||||||
|
{
|
||||||
|
private readonly static ExifTool exifTool = new();
|
||||||
|
private readonly static GeneralUseHelpers guhf = new();
|
||||||
|
public async static Task<Dictionary<string, string>> ExtractAsync(string fullPath)
|
||||||
|
{
|
||||||
|
// Get all relevant metadata
|
||||||
|
Dictionary<string, string> fileMetadata = new(await
|
||||||
|
exifTool.ExtractAllMetadataAsync(fullPath));
|
||||||
|
|
||||||
|
// Add in a MD5 hint
|
||||||
|
string md5sum = await GetFileMD5Async(fullPath);
|
||||||
|
fileMetadata?.Add("_shadow:fileHash", md5sum);
|
||||||
|
|
||||||
|
return fileMetadata ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute MD5 checksum of a file
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="fullPath">Input file absolute path</param>
|
||||||
|
/// <returns>MD5 hexstring</returns>
|
||||||
|
public static async Task<string> GetFileMD5Async(string fullPath)
|
||||||
|
{
|
||||||
|
string fallbackValue = String.Empty;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (File.Exists(fullPath))
|
||||||
|
using (MD5 md5 = MD5.Create())
|
||||||
|
{
|
||||||
|
using (FileStream stream = File.OpenRead(fullPath))
|
||||||
|
{
|
||||||
|
byte[] hashBytes = await md5.ComputeHashAsync(stream);
|
||||||
|
string hashString = Convert.ToHexStringLower(hashBytes);
|
||||||
|
return hashString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else return fallbackValue;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return fallbackValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Compute MD5 checksum of a string
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="input">Input string for the MD5 hash</param>
|
||||||
|
/// <returns>MD5 hexstring</returns>
|
||||||
|
public static string GetStringMD5(string input)
|
||||||
|
{
|
||||||
|
// https://stackoverflow.com/a/24031467
|
||||||
|
byte[] inputBytes = System.Text.Encoding.ASCII.GetBytes(input);
|
||||||
|
byte[] hashBytes = MD5.HashData(inputBytes);
|
||||||
|
string hashString = Convert.ToHexStringLower(hashBytes);
|
||||||
|
|
||||||
|
return hashString;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ public class Seeder
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
Console.WriteLine("Upgrade detected. Make sure you're using the most recent migrations.\n" +
|
Console.WriteLine("Upgrade detected. Make sure you're using the most recent migrations.\n" +
|
||||||
"If not, apply them with `dotnet ef database update`.\n");
|
"If not, apply them automatically or with `dotnet ef database update`.\n");
|
||||||
|
|
||||||
lastVersion.Value = ThisAssembly.Git.Commit;
|
lastVersion.Value = ThisAssembly.Git.Commit;
|
||||||
lastVersionDate.Value = ThisAssembly.Git.CommitDate;
|
lastVersionDate.Value = ThisAssembly.Git.CommitDate;
|
||||||
@@ -84,25 +84,34 @@ public class Seeder
|
|||||||
if (adminCount == 0 && userCount > 0)
|
if (adminCount == 0 && userCount > 0)
|
||||||
Console.WriteLine("[Warn]: No admin accounts exist. Consider creating one with `Shadow addUser`.\n");
|
Console.WriteLine("[Warn]: No admin accounts exist. Consider creating one with `Shadow addUser`.\n");
|
||||||
|
|
||||||
// Ensure [Unknown Album], [Unknown Artist] exist
|
// Ensure [Unknown Artist], [Unknown Album] exist
|
||||||
Album unknownAlbum = db.Albums.FirstOrDefault(a => a.Name == "[Unknown Album]") ?? new Album()
|
|
||||||
{
|
|
||||||
Name = "[Unknown Album]",
|
|
||||||
Uri = "00000000000000000000000000000000"
|
|
||||||
};
|
|
||||||
|
|
||||||
Artist unknownArtist = db.Artists.FirstOrDefault(a => a.Name == "[Unknown Artist]") ?? new Artist()
|
Artist unknownArtist = db.Artists.FirstOrDefault(a => a.Name == "[Unknown Artist]") ?? new Artist()
|
||||||
{
|
{
|
||||||
Name = "[Unknown Artist]",
|
Name = "[Unknown Artist]",
|
||||||
NormalizedName = "[unknown artist]"
|
NormalizedName = "[unknown artist]"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Update works both as an Add and Update.
|
||||||
|
db.Update(unknownArtist);
|
||||||
|
db.SaveChanges();
|
||||||
|
|
||||||
|
Album unknownAlbum = db.Albums.FirstOrDefault(a => a.Name == "[Unknown Album]") ?? new Album()
|
||||||
|
{
|
||||||
|
Name = "[Unknown Album]",
|
||||||
|
Uri = "00000000000000000000000000000000",
|
||||||
|
Artist = unknownArtist
|
||||||
|
};
|
||||||
|
db.Update(unknownAlbum);
|
||||||
|
|
||||||
|
// Add [Unknown Album] to [Unknown Artist]
|
||||||
|
unknownArtist.Albums.Add(unknownAlbum);
|
||||||
|
db.Update(unknownArtist);
|
||||||
|
|
||||||
runs ??= new Global() { Key = "runs", Value = "0" };
|
runs ??= new Global() { Key = "runs", Value = "0" };
|
||||||
if (int.TryParse(runs.Value, out int runsInt))
|
if (int.TryParse(runs.Value, out int runsInt))
|
||||||
runs.Value = $"{runsInt + 1}";
|
runs.Value = $"{runsInt + 1}";
|
||||||
|
|
||||||
// UpdateRange works both as an Add and Update.
|
db.Update(runs);
|
||||||
db.UpdateRange(unknownAlbum, unknownArtist, runs);
|
|
||||||
db.SaveChanges();
|
db.SaveChanges();
|
||||||
|
|
||||||
return shutdown;
|
return shutdown;
|
||||||
@@ -118,10 +127,9 @@ public class Seeder
|
|||||||
List<string> appliedMigrations = dbContext.Database.GetAppliedMigrations().ToList();
|
List<string> appliedMigrations = dbContext.Database.GetAppliedMigrations().ToList();
|
||||||
|
|
||||||
bool hasMissingMigrations = availableMigrations.Count > appliedMigrations.Count;
|
bool hasMissingMigrations = availableMigrations.Count > appliedMigrations.Count;
|
||||||
bool doMigrationsMatch = availableMigrations.SequenceEqual(appliedMigrations);
|
|
||||||
if (hasMissingMigrations)
|
if (hasMissingMigrations)
|
||||||
{
|
{
|
||||||
bool userMigrationConsent = false;
|
bool userMigrationConsent = true; // apply migrations by default
|
||||||
Console.WriteLine("\n" +
|
Console.WriteLine("\n" +
|
||||||
"========================================\n" +
|
"========================================\n" +
|
||||||
"[Warn] Database migrations missing!\n" +
|
"[Warn] Database migrations missing!\n" +
|
||||||
@@ -129,19 +137,30 @@ public class Seeder
|
|||||||
if (appliedMigrations.Count == 0)
|
if (appliedMigrations.Count == 0)
|
||||||
{
|
{
|
||||||
Console.WriteLine( "Empty database detected. Applying migrations is recommended, and required if this is the first run of Shadow.");
|
Console.WriteLine( "Empty database detected. Applying migrations is recommended, and required if this is the first run of Shadow.");
|
||||||
userMigrationConsent = Cli.YesNoPrompt("Do you want to apply migrations automatically? [Y/n]: ", true);
|
// userMigrationConsent = Cli.YesNoPrompt("Do you want to apply migrations automatically? [Y/n]: ", true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Detected existing {appliedMigrations.Count} migrations. Backing up the database before applying migrations is recommended.");
|
Console.WriteLine($"Detected existing {appliedMigrations.Count} migrations. Backing up the database before applying migrations is recommended.\n" +
|
||||||
userMigrationConsent = Cli.YesNoPrompt("Do you want to apply migrations automatically? [y/N]: ", false);
|
"Waiting 15 seconds.");
|
||||||
|
Thread.Sleep(15_000);
|
||||||
|
// userMigrationConsent = Cli.YesNoPrompt("Do you want to apply migrations automatically? [y/N]: ", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do we have user permission to perform migration?
|
// Do we have user permission to perform migration?
|
||||||
if (userMigrationConsent)
|
if (userMigrationConsent)
|
||||||
{
|
{
|
||||||
dbContext.Database.Migrate();
|
try
|
||||||
dbContext.SaveChanges();
|
{
|
||||||
|
dbContext.Database.Migrate();
|
||||||
|
dbContext.SaveChanges();
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
Console.WriteLine("Error! Unable to process migration automatically.\n" +
|
||||||
|
$"Error message was: {e.Message}\n\n" +
|
||||||
|
$"Consider ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -177,4 +196,41 @@ public class Seeder
|
|||||||
return migrationSuccess;
|
return migrationSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the library needs a full rescan
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>True if full rescan is needed</returns>
|
||||||
|
/// <exception cref="MissingFieldException">Thrown when either last library state, path to music library or cache is unknown</exception>
|
||||||
|
public async Task<bool> ScanPrefetchAsync()
|
||||||
|
{
|
||||||
|
bool scanNecessary = false;
|
||||||
|
Global? lastLibraryState = await db.Globals.FirstOrDefaultAsync(g => g.Key == "libraryState");
|
||||||
|
Global? libraryPath = await db.Globals.FirstOrDefaultAsync(g => g.Key == "musicLibraryPath");
|
||||||
|
Global? cachePath = await db.Globals.FirstOrDefaultAsync(g => g.Key == "musicThumbnailPath");
|
||||||
|
|
||||||
|
if (libraryPath is null || cachePath is null || lastLibraryState is null)
|
||||||
|
{
|
||||||
|
throw new MissingFieldException("[Error] Missing libraryState, musicLibraryPath, musicThumbnailPath. Please rerun the setup wizard with `Shadow setupWizard`.");
|
||||||
|
}
|
||||||
|
LibraryWatcher lw = new(libraryPath.Value!, [cachePath.Value!], db);
|
||||||
|
|
||||||
|
// Get library contents
|
||||||
|
List<string> currentLibraryStateList = await lw.GetAllMultimediaAsync();
|
||||||
|
// Compute their hash
|
||||||
|
string currentLibraryStateString = string.Join("\n", currentLibraryStateList);
|
||||||
|
string currentLibraryState = MetadataExtractor.GetStringMD5(currentLibraryStateString);
|
||||||
|
|
||||||
|
// Compare against last known library state
|
||||||
|
if (currentLibraryState != lastLibraryState.Value)
|
||||||
|
scanNecessary = true;
|
||||||
|
|
||||||
|
// The contents changed? Initiate a full rescan, then call LibraryWatcher.
|
||||||
|
if (scanNecessary)
|
||||||
|
await lw.PerformFullScanAsync();
|
||||||
|
// State seems identical? Launch just the LibraryWatcher.
|
||||||
|
// TODO: lw.Watch()...
|
||||||
|
|
||||||
|
return scanNecessary;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
"Microsoft.AspNetCore": "Warning"
|
"Microsoft.AspNetCore": "Warning",
|
||||||
|
"Microsoft.EntityFrameworkCore.Database.Command": "Warning",
|
||||||
|
"Microsoft.AspNetCore.HttpLogging.HttpLoggingMiddleware": "Information"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
|
|||||||
Reference in New Issue
Block a user