Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dbda9a3897 | |||
| a987e438c4 | |||
| a150d34c6c | |||
| 34e292bea9 | |||
| c9bcea5f98 | |||
| 6c9e4809a9 | |||
| 4c5bd1ccde | |||
| 5a2639cb59 | |||
| fd9df41f51 | |||
| 7fdea3491f | |||
| 3cde57dca8 | |||
| 8c93429647 | |||
| 5721c387ea | |||
| a624702813 | |||
| 9c67bfd8a8 | |||
| fbec3305b1 | |||
| fa792be9cc | |||
| 6f2e8d6e17 | |||
| eb1f19a96e | |||
| bb766acf4e | |||
| 95688cccfa | |||
| e2485cec44 | |||
| df1562f5e7 | |||
| e00e9cf185 | |||
| f0d2b9de71 | |||
| f0a0bee22b | |||
| 0b73c055aa | |||
| d4c025511d | |||
| b2a4faec76 | |||
| 7d27aed314 | |||
| 961f99b744 | |||
| b1f5c5f604 | |||
| ddb0bf27f8 | |||
| d491767de4 | |||
| 8755276b48 | |||
| fd41d25c4c | |||
| 5e3ca1861d | |||
| db7350b550 | |||
| f823a99984 | |||
| 2823452198 | |||
| 0ba2d4e8b9 | |||
| 4fdd660988 | |||
| b6fe9a070c | |||
| 6e4eeafe13 | |||
| 6f320acb7c | |||
| 9dd7ffa2b6 | |||
| fbcbd41809 | |||
| 01d315e25e | |||
| caf9f08d8a | |||
| 7227eb9420 | |||
| 12c4f56a22 | |||
| 6d650b7962 | |||
| 73bb417bde | |||
| b0277077f4 | |||
| 3edf6424d0 | |||
| dcb51448aa | |||
| 9d90247bf8 | |||
| 86e54e086a | |||
| fd3ce9f96a | |||
| a89580ac70 | |||
| 8900afdfd2 | |||
| 4b3fa78336 | |||
| 71656aa940 | |||
| 562bc87822 | |||
| ebb15b7cdb | |||
| 270b7d059c | |||
| 23c50de468 | |||
| ececcb5ba3 | |||
| 8658dd34ab | |||
| c17c9cd329 | |||
| 37cfda84b5 | |||
| 3ee17a77a8 | |||
| 522be59973 | |||
| 712a0a9a4a | |||
| 74569d85b6 | |||
| b660f8361f | |||
| d460b5d5d6 | |||
| 4a1c1f1f78 | |||
| 66b5944f92 | |||
| 1643f78720 | |||
| 06bb52dfa5 | |||
| ab89d1602f |
@@ -0,0 +1,21 @@
|
|||||||
|
# Ignore build artifacts
|
||||||
|
target/
|
||||||
|
pkg/
|
||||||
|
|
||||||
|
# Ignore git directory
|
||||||
|
.git/
|
||||||
|
|
||||||
|
.github/
|
||||||
|
|
||||||
|
# Ignore environment files (configure via docker-compose instead)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# Ignore IDE/editor specific files
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# Ignore OS specific files
|
||||||
|
*.DS_Store
|
||||||
|
|
||||||
|
# Add any other files/directories you don't need in the image
|
||||||
|
# e.g., logs/, tmp/
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
|
||||||
|
ROOT_DIRECTORY=/home/icarus/mydata
|
||||||
|
POSTGRES_MAIN_USER=icarus
|
||||||
|
POSTGRES_MAIN_PASSWORD=password
|
||||||
|
POSTGRES_MAIN_DB=icarus_db
|
||||||
|
POSTGRES_MAIN_HOST=main_db
|
||||||
|
DATABASE_URL=postgres://${POSTGRES_MAIN_USER}:${POSTGRES_MAIN_PASSWORD}@${POSTGRES_MAIN_HOST}:5432/${POSTGRES_MAIN_DB}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
SECRET_MAIN_KEY=refero34o8rfhfjn983thf39fhc943rf923n3h
|
||||||
|
ROOT_DIRECTORY=/home/icarus/mydata
|
||||||
|
POSTGRES_MAIN_USER=icarus
|
||||||
|
POSTGRES_MAIN_PASSWORD=password
|
||||||
|
POSTGRES_MAIN_DB=icarus_db
|
||||||
|
POSTGRES_MAIN_HOST=localhost
|
||||||
|
DATABASE_URL=postgres://${POSTGRES_MAIN_USER}:${POSTGRES_MAIN_PASSWORD}@${POSTGRES_MAIN_HOST}:5432/${POSTGRES_MAIN_DB}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
name: Release Tagging
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "v0.2" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # Important for git describe --tags
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: 1.88.0
|
||||||
|
components: cargo
|
||||||
|
|
||||||
|
- name: Extract Version from Cargo.toml
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
VERSION=$(grep '^version = "' Cargo.toml | sed -E 's/version = "([^"]+)"/\1/')
|
||||||
|
PROJECT_COMMIT_HASH=$(git rev-parse HEAD | cut -c 1-10)
|
||||||
|
BRANCH_REF="${GITHUB_REF}"
|
||||||
|
BRANCH_NAME=$(echo "$BRANCH_REF" | cut -d '/' -f 3)
|
||||||
|
PROJECT_TAG_RELEASE="v$VERSION-$BRANCH_NAME-$PROJECT_COMMIT_HASH"
|
||||||
|
echo "::set-output name=project_tag_release::$PROJECT_TAG_RELEASE-961"
|
||||||
|
echo "Version: $VERSION"
|
||||||
|
echo "Hash: $PROJECT_COMMIT_HASH"
|
||||||
|
echo "Branch: $BRANCH_NAME"
|
||||||
|
echo "Tag Release: $PROJECT_TAG_RELEASE"
|
||||||
|
|
||||||
|
- name: Print version
|
||||||
|
id: print_version
|
||||||
|
run: |
|
||||||
|
echo "Printing version"
|
||||||
|
echo "Version: ${{ steps.version.outputs.project_tag_release }}"
|
||||||
|
|
||||||
|
- name: Create GitHub Release
|
||||||
|
uses: actions/create-release@v1
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
tag_name: ${{ steps.version.outputs.project_tag_release }}
|
||||||
|
release_name: Release ${{ steps.version.outputs.project_tag_release }}
|
||||||
|
body: |
|
||||||
|
Release of version ${{ steps.version.outputs.project_tag_release }}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
name: Rust CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ "v0.2" ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ "v0.2" ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:17.5
|
||||||
|
env:
|
||||||
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||||
|
POSTGRES_USER: ${{ secrets.POSTGRES_USER }}
|
||||||
|
POSTGRES_DB: ${{ secrets.POSTGRES_DB }}
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust 1.88.0
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: 1.88.0
|
||||||
|
components: clippy, rustfmt
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Install libpq
|
||||||
|
run: sudo apt-get install -y libpq-dev
|
||||||
|
|
||||||
|
- name: Setup test database
|
||||||
|
env:
|
||||||
|
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}"
|
||||||
|
run: |
|
||||||
|
# Wait for PostgreSQL to be ready
|
||||||
|
for i in {1..10}; do
|
||||||
|
pg_isready -U ${{ secrets.POSTGRES_USER }} -d ${{ secrets.POSTGRES_DB }} && break
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
|
# Run database migrations (if you use sqlx migrations)
|
||||||
|
cargo install sqlx-cli --no-default-features --features native-tls,postgres
|
||||||
|
sqlx migrate run --database-url $DATABASE_URL
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/.ssh
|
||||||
|
echo "${{ secrets.MYREPO_TOKEN }}" > ~/.ssh/gitea_deploy_key
|
||||||
|
chmod 600 ~/.ssh/gitea_deploy_key
|
||||||
|
ssh-keyscan ${{ vars.MYHOST }} >> ~/.ssh/known_hosts
|
||||||
|
eval $(ssh-agent -s)
|
||||||
|
ssh-add -v ~/.ssh/gitea_deploy_key
|
||||||
|
cargo build --verbose --release
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
env:
|
||||||
|
ROOT_DIRECTORY: "/tmp"
|
||||||
|
DATABASE_URL: "postgres://${{ secrets.POSTGRES_USER }}:${{ secrets.POSTGRES_PASSWORD }}@localhost:5432/${{ secrets.POSTGRES_DB }}"
|
||||||
|
run: cargo test --verbose
|
||||||
|
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
- name: Run rustfmt
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
################################################################################
|
# Added by cargo
|
||||||
# This .gitignore file was automatically created by Microsoft(R) Visual Studio.
|
|
||||||
################################################################################
|
|
||||||
|
|
||||||
/.vs/Icarus
|
/target
|
||||||
/bin/*
|
.env
|
||||||
/bin/Debug/netcoreapp2.2
|
.env.docker
|
||||||
/Migrations
|
.env.local
|
||||||
/obj
|
|
||||||
/obj/*
|
.DS_STORE
|
||||||
/Icarus.txt
|
Storage/
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"dev": {
|
||||||
|
"migrations": "./migrations"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"migrations": "./test_migrations"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
using Icarus.Authorization;
|
|
||||||
|
|
||||||
namespace Icarus.Authorization.Handlers
|
|
||||||
{
|
|
||||||
public class HasScopeHandler : AuthorizationHandler<HasScopeRequirement>
|
|
||||||
{
|
|
||||||
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement)
|
|
||||||
{
|
|
||||||
if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer))
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
var scopes = context.User.FindFirst(c =>
|
|
||||||
c.Type == "scope" && c.Issuer == requirement.Issuer).Value.Split(' ');
|
|
||||||
|
|
||||||
if (scopes.Any(s => s == requirement.Scope))
|
|
||||||
{
|
|
||||||
context.Succeed(requirement);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
|
|
||||||
namespace Icarus.Authorization
|
|
||||||
{
|
|
||||||
public class HasScopeRequirement : IAuthorizationRequirement
|
|
||||||
{
|
|
||||||
public string Issuer { get; }
|
|
||||||
public string Scope { get; }
|
|
||||||
|
|
||||||
|
|
||||||
public HasScopeRequirement(string scope, string issuer)
|
|
||||||
{
|
|
||||||
Scope = scope ?? throw new ArgumentNullException(nameof(scope));
|
|
||||||
Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
# Contributing
|
|
||||||
|
|
||||||
When contributing to this repository, please first discuss the change you wish to make via issue,
|
|
||||||
email, or any other method with the owners of this repository before making a change.
|
|
||||||
|
|
||||||
Please note the project has a code of conduct, please follow it in all your interactions with the project.
|
|
||||||
|
|
||||||
## Code of Conduct
|
|
||||||
|
|
||||||
### Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, I as
|
|
||||||
aintainers pledge to making participation in this project and
|
|
||||||
this community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
||||||
nationality, personal appearance, race, religion, or sexual identity and
|
|
||||||
orientation.
|
|
||||||
|
|
||||||
### Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
|
||||||
include:
|
|
||||||
|
|
||||||
* Using welcoming and inclusive language
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic
|
|
||||||
address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
### Maintainers Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## How You Can Contribute
|
|
||||||
|
|
||||||
The project is always looking for people to help in anyway they can. The following would be greatly appreciated:
|
|
||||||
|
|
||||||
* Developers
|
|
||||||
* Graphic Designers
|
|
||||||
* Documenters
|
|
||||||
|
|
||||||
### Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
||||||
available at [http://contributor-covenant.org/version/1/4][version]
|
|
||||||
|
|
||||||
[homepage]: http://contributor-covenant.org
|
|
||||||
[version]: http://contributor-covenant.org/version/1/4/
|
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
[package]
|
||||||
|
name = "icarus"
|
||||||
|
version = "0.1.94"
|
||||||
|
edition = "2024"
|
||||||
|
rust-version = "1.88"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.8.4", features = ["multipart"] }
|
||||||
|
serde = { version = "1.0.219", features = ["derive"] }
|
||||||
|
serde_json = { version = "1.0.140" }
|
||||||
|
tower = { version = "0.5.2" }
|
||||||
|
tokio = { version = "1.45.1", features = ["full"] }
|
||||||
|
tokio-util = { version = "0.7.15" }
|
||||||
|
tower-http = { version = "0.6.6", features = ["timeout"] }
|
||||||
|
tracing-subscriber = "0.3.19"
|
||||||
|
futures = { version = "0.3.31" }
|
||||||
|
uuid = { version = "1.17.0", features = ["v4", "serde"] }
|
||||||
|
sqlx = { version = "0.8.6", features = ["postgres", "runtime-tokio-native-tls", "time", "uuid"] }
|
||||||
|
time = { version = "0.3.41", features = ["formatting", "macros", "parsing", "serde"] }
|
||||||
|
icarus_meta = { git = "ssh://git@git.kundeng.us/phoenix/icarus_meta.git", tag = "v0.3.0-devel-f4b71de969-680" }
|
||||||
|
icarus_models = { git = "ssh://git@git.kundeng.us/phoenix/icarus_models.git", tag = "v0.4.5-devel-655d05dabb-111" }
|
||||||
|
icarus_envy = { git = "ssh://git@git.kundeng.us/phoenix/icarus_envy.git", tag = "v0.3.0-devel-d73fba9899-006" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
common-multipart-rfc7578 = { version = "0.7.0" }
|
||||||
|
url = { version = "2.5.4" }
|
||||||
|
tempfile = { version = "3.20.0" }
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using Org.BouncyCastle.Crypto.Parameters;
|
|
||||||
using Org.BouncyCastle.OpenSsl;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Icarus.Certs
|
|
||||||
{
|
|
||||||
public class SigningIssuerCertificate : IDisposable
|
|
||||||
{
|
|
||||||
private readonly RSACryptoServiceProvider _rsa;
|
|
||||||
|
|
||||||
public SigningIssuerCertificate()
|
|
||||||
{
|
|
||||||
_rsa = new RSACryptoServiceProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
public RsaSecurityKey GetIssuerSigningKey(string publicKeyPath)
|
|
||||||
{
|
|
||||||
var file = publicKeyPath;
|
|
||||||
var publicKey = System.IO.File.ReadAllText(file);
|
|
||||||
|
|
||||||
using (var reader = System.IO.File.OpenText(file))
|
|
||||||
{
|
|
||||||
var pem = new PemReader(reader);
|
|
||||||
var o = (RsaKeyParameters)pem.ReadObject();
|
|
||||||
var parameters = new RSAParameters();
|
|
||||||
parameters.Modulus = o.Modulus.ToByteArray();
|
|
||||||
parameters.Exponent = o.Exponent.ToByteArray();
|
|
||||||
_rsa.ImportParameters(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RsaSecurityKey(_rsa);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_rsa?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public class SigningAudienceCertificate : IDisposable
|
|
||||||
{
|
|
||||||
private readonly RSACryptoServiceProvider _rsa;
|
|
||||||
|
|
||||||
public SigningAudienceCertificate()
|
|
||||||
{
|
|
||||||
_rsa = new RSACryptoServiceProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
public SigningCredentials GetAudienceSigningKey(string keyPath)
|
|
||||||
{
|
|
||||||
var file = keyPath;
|
|
||||||
var publicKey = System.IO.File.ReadAllText(file);
|
|
||||||
|
|
||||||
using (var reader = System.IO.File.OpenText(file))
|
|
||||||
{
|
|
||||||
var pem = new PemReader(reader);
|
|
||||||
var o = (RsaKeyParameters)pem.ReadObject();
|
|
||||||
var parameters = new RSAParameters();
|
|
||||||
parameters.Modulus = o.Modulus.ToByteArray();
|
|
||||||
parameters.Exponent = o.Exponent.ToByteArray();
|
|
||||||
_rsa.ImportParameters(parameters);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new SigningCredentials(
|
|
||||||
key: new RsaSecurityKey(_rsa),
|
|
||||||
algorithm: SecurityAlgorithms.RsaSha256);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_rsa?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace Icarus.Constants
|
|
||||||
{
|
|
||||||
public class DirectoryPaths
|
|
||||||
{
|
|
||||||
public static string CoverArtPath =>
|
|
||||||
Directory.GetCurrentDirectory() + "/Images/Stock/CoverArt.png";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
|
||||||
{
|
|
||||||
public class AlbumManager : BaseManager
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private AlbumContext _albumContext;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public AlbumManager(IConfiguration config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
_albumContext = new AlbumContext(_connectionString);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public void SaveAlbumToDatabase(ref Song song)
|
|
||||||
{
|
|
||||||
_logger.Info("Starting process to save the album record of the song to the database");
|
|
||||||
|
|
||||||
var album = new Album();
|
|
||||||
|
|
||||||
album.Title = song.AlbumTitle;
|
|
||||||
album.AlbumArtist = song.Artist;
|
|
||||||
album.Year = song.Year.Value;
|
|
||||||
var albumTitle = song.AlbumTitle;
|
|
||||||
var albumArtist = song.Artist;
|
|
||||||
|
|
||||||
var albumRetrieved = _albumContext.Albums.FirstOrDefault(alb => alb.Title.Equals(albumTitle) && alb.AlbumArtist.Equals(albumArtist));
|
|
||||||
|
|
||||||
if (albumRetrieved == null)
|
|
||||||
{
|
|
||||||
album.SongCount = 1;
|
|
||||||
_albumContext.Add(album);
|
|
||||||
_albumContext.SaveChanges();
|
|
||||||
|
|
||||||
Console.WriteLine($"Album Id {album.AlbumID}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
album.AlbumID = albumRetrieved.AlbumID;
|
|
||||||
}
|
|
||||||
|
|
||||||
song.AlbumID = album.AlbumID;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void DeleteAlbumFromDatabase(Song song)
|
|
||||||
{
|
|
||||||
var album = _albumContext.Albums.FirstOrDefault(alb => alb.Title.Equals(song.AlbumTitle));
|
|
||||||
|
|
||||||
if (album == null)
|
|
||||||
{
|
|
||||||
_logger.Info("Cannot delete the album record because it does not exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteAlbumFromDb(album);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Album UpdateAlbumInDatabase(Song oldSong, Song newSong)
|
|
||||||
{
|
|
||||||
var albumRecord = _albumContext.Albums.FirstOrDefault(alb => alb.Title.Equals(oldSong.AlbumTitle));
|
|
||||||
var oldAlbumTitle = oldSong.AlbumTitle;
|
|
||||||
var oldAlbumArtist = oldSong.Artist;
|
|
||||||
var newAlbumTitle = newSong.AlbumTitle;
|
|
||||||
var newAlbumArtist = newSong.Artist;
|
|
||||||
|
|
||||||
var info = string.Empty;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(newAlbumArtist))
|
|
||||||
newAlbumArtist = oldAlbumArtist;
|
|
||||||
if (string.IsNullOrEmpty(newAlbumTitle))
|
|
||||||
newAlbumTitle = oldAlbumTitle;
|
|
||||||
|
|
||||||
if ((string.IsNullOrEmpty(newAlbumTitle) && string.IsNullOrEmpty(newAlbumArtist) ||
|
|
||||||
oldAlbumTitle.Equals(newAlbumTitle) && oldAlbumArtist.Equals(newAlbumArtist)))
|
|
||||||
{
|
|
||||||
_logger.Info("No change to the song's album");
|
|
||||||
return albumRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
info = "Change to the song's album";
|
|
||||||
_logger.Info(info);
|
|
||||||
|
|
||||||
var existingAlbumRecord = _albumContext.Albums.FirstOrDefault(alb => alb.Title.Equals(oldSong.AlbumTitle));
|
|
||||||
if (existingAlbumRecord == null)
|
|
||||||
{
|
|
||||||
_logger.Info("Creating new album record");
|
|
||||||
|
|
||||||
var newAlbumRecord = new Album
|
|
||||||
{
|
|
||||||
Title = newAlbumTitle,
|
|
||||||
AlbumArtist = newAlbumArtist,
|
|
||||||
Year = newSong.Year.Value
|
|
||||||
};
|
|
||||||
|
|
||||||
_albumContext.Add(newAlbumRecord);
|
|
||||||
_albumContext.SaveChanges();
|
|
||||||
|
|
||||||
return newAlbumRecord;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.Info("Updating existing album record");
|
|
||||||
|
|
||||||
existingAlbumRecord = _albumContext.Albums.FirstOrDefault(alb => alb.Title.Equals(newSong.AlbumTitle));
|
|
||||||
existingAlbumRecord.AlbumArtist = newAlbumArtist;
|
|
||||||
|
|
||||||
_albumContext.Update(existingAlbumRecord);
|
|
||||||
_albumContext.SaveChanges();
|
|
||||||
|
|
||||||
return existingAlbumRecord;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteAlbumFromDb(Album album)
|
|
||||||
{
|
|
||||||
if (SongsInAlbum(album) <= 1)
|
|
||||||
{
|
|
||||||
_albumContext.Remove(album);
|
|
||||||
_albumContext.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int SongsInAlbum(Album album)
|
|
||||||
{
|
|
||||||
var sngContext = new SongContext(_connectionString);
|
|
||||||
var songs = sngContext.Songs.Where(sng => sng.AlbumID == album.AlbumID).ToList();
|
|
||||||
|
|
||||||
return songs.Count;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
|
||||||
{
|
|
||||||
public class ArtistManager : BaseManager
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private ArtistContext _artistContext;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public ArtistManager(IConfiguration config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
_artistContext = new ArtistContext(_connectionString);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public void SaveArtistToDatabase(ref Song song)
|
|
||||||
{
|
|
||||||
_logger.Info("Starting process to save the artist record of the song to the database");
|
|
||||||
|
|
||||||
var artist = new Artist();
|
|
||||||
|
|
||||||
artist.Name = song.Artist;
|
|
||||||
artist.SongCount = 1;
|
|
||||||
var artistTitle = artist.Name;
|
|
||||||
|
|
||||||
var artistRetrieved = _artistContext.Artists.FirstOrDefault(art => art.Name.Equals(artistTitle));
|
|
||||||
|
|
||||||
if (artistRetrieved == null)
|
|
||||||
{
|
|
||||||
artist.SongCount = 1;
|
|
||||||
_artistContext.Add(artist);
|
|
||||||
_artistContext.SaveChanges();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
artist.ArtistID = artistRetrieved.ArtistID;
|
|
||||||
}
|
|
||||||
|
|
||||||
song.ArtistID = artist.ArtistID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Artist UpdateArtistInDatabase(Song oldSongRecord, Song newSongRecord)
|
|
||||||
{
|
|
||||||
var oldArtistRecord = _artistContext.Artists.FirstOrDefault(art => art.Name.Equals(oldSongRecord.AlbumTitle));
|
|
||||||
var oldArtistName = oldArtistRecord.Name;
|
|
||||||
var newArtistName = newSongRecord.Artist;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(newArtistName) || oldArtistName.Equals(newArtistName))
|
|
||||||
{
|
|
||||||
_logger.Info("No change to the song's Artist");
|
|
||||||
return oldArtistRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Info("Change to the song's record found");
|
|
||||||
|
|
||||||
if (oldArtistRecord.SongCount <= 1)
|
|
||||||
{
|
|
||||||
_logger.Info("Deleting artist record that no longer has any songs");
|
|
||||||
|
|
||||||
_artistContext.Remove(oldArtistRecord);
|
|
||||||
_artistContext.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(_artistContext.Artists.FirstOrDefault(art => art.Name.Equals(oldSongRecord.AlbumTitle)) != null))
|
|
||||||
{
|
|
||||||
_logger.Info("Creating new artist record");
|
|
||||||
|
|
||||||
var newArtistRecord = new Artist
|
|
||||||
{
|
|
||||||
Name = newArtistName
|
|
||||||
};
|
|
||||||
|
|
||||||
_artistContext.Add(newArtistRecord);
|
|
||||||
_artistContext.SaveChanges();
|
|
||||||
|
|
||||||
return newArtistRecord;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.Info("Updating existing artist record");
|
|
||||||
|
|
||||||
var existingArtistRecord = _artistContext.Artists.FirstOrDefault(art => art.Name.Equals(newSongRecord.AlbumTitle));
|
|
||||||
|
|
||||||
_artistContext.Update(existingArtistRecord);
|
|
||||||
_artistContext.SaveChanges();
|
|
||||||
|
|
||||||
return existingArtistRecord;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteArtistFromDatabase(Song song)
|
|
||||||
{
|
|
||||||
if (!(_artistContext.Artists.FirstOrDefault(art => art.Name.Equals(song.Artist)) != null))
|
|
||||||
{
|
|
||||||
_logger.Info("Cannot delete the artist record because it does not exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var artist = _artistContext.Artists.FirstOrDefault(art => art.Name.Equals(song.Artist));
|
|
||||||
|
|
||||||
if (SongsOfArtist(artist) <= 1)
|
|
||||||
{
|
|
||||||
_artistContext.Remove(artist);
|
|
||||||
_artistContext.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int SongsOfArtist(Artist artist)
|
|
||||||
{
|
|
||||||
var sngContext = new SongContext(_connectionString);
|
|
||||||
var songs = sngContext.Songs.Where(sng => sng.ArtistID == artist.ArtistID).ToList();
|
|
||||||
|
|
||||||
return songs.Count;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using NLog;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
|
||||||
{
|
|
||||||
public class BaseManager
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
protected static Logger _logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
|
|
||||||
protected IConfiguration _config;
|
|
||||||
protected string _connectionString;
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using Icarus.Constants;
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Types;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
|
||||||
{
|
|
||||||
public class CoverArtManager : BaseManager
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private string _rootCoverArtPath;
|
|
||||||
private CoverArtContext _coverArtContext;
|
|
||||||
private byte[] _stockCoverArt = null;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public CoverArtManager(IConfiguration config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
_rootCoverArtPath = _config.GetValue<string>("CoverArtPath");
|
|
||||||
Initialize();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public void SaveCoverArtToDatabase(ref Song song, ref CoverArt coverArt)
|
|
||||||
{
|
|
||||||
_logger.Info("Saving cover art record to the database");
|
|
||||||
_coverArtContext.Add(coverArt);
|
|
||||||
_coverArtContext.SaveChanges();
|
|
||||||
|
|
||||||
song.CoverArtID = coverArt.CoverArtID;
|
|
||||||
}
|
|
||||||
public void DeleteCoverArtFromDatabase(CoverArt coverArt)
|
|
||||||
{
|
|
||||||
_logger.Info("Attempting to delete cover art from the database");
|
|
||||||
|
|
||||||
_coverArtContext.Remove(coverArt);
|
|
||||||
_coverArtContext.SaveChanges();
|
|
||||||
}
|
|
||||||
public void DeleteCoverArt(CoverArt coverArt)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var stockCoverArtPath = _rootCoverArtPath + "CoverArt.png";
|
|
||||||
if (!string.Equals(stockCoverArtPath, coverArt.ImagePath,
|
|
||||||
StringComparison.CurrentCultureIgnoreCase))
|
|
||||||
{
|
|
||||||
_logger.Info("Song does not contain the stock cover art");
|
|
||||||
File.Delete(coverArt.ImagePath);
|
|
||||||
_logger.Info("Cover art deleted from the filesystem");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.Info("Song contains the stock cover art");
|
|
||||||
_logger.Info("Will not delete from from the filesystem");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CoverArt SaveCoverArt(Song song)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var dirMgr = new DirectoryManager(_rootCoverArtPath);
|
|
||||||
var defaultExtension = ".png";
|
|
||||||
dirMgr.CreateDirectory(song);
|
|
||||||
|
|
||||||
var coverArt = new CoverArt
|
|
||||||
{
|
|
||||||
SongTitle = song.Title
|
|
||||||
};
|
|
||||||
|
|
||||||
var segment = coverArt.GenerateFilename(0);
|
|
||||||
var imagePath = dirMgr.SongDirectory + segment + defaultExtension;
|
|
||||||
|
|
||||||
coverArt.ImagePath = imagePath;
|
|
||||||
|
|
||||||
var metaData = new MetadataRetriever();
|
|
||||||
var imgBytes = metaData.RetrieveCoverArtBytes(song);
|
|
||||||
|
|
||||||
if (imgBytes != null)
|
|
||||||
{
|
|
||||||
_logger.Info("Saving cover art to the filesystem");
|
|
||||||
File.WriteAllBytes(coverArt.ImagePath, imgBytes);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.Info("Song has no cover art, applying stock cover art");
|
|
||||||
coverArt.ImagePath = _rootCoverArtPath + $"{segment}{defaultExtension}";
|
|
||||||
metaData.UpdateCoverArt(song, coverArt);
|
|
||||||
File.WriteAllBytes(coverArt.ImagePath, _stockCoverArt);
|
|
||||||
}
|
|
||||||
|
|
||||||
return coverArt;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CoverArt SaveCoverArt(IFormFile data, Song song)
|
|
||||||
{
|
|
||||||
var cover = new CoverArt { SongTitle = song.Title };
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var dirMgr = new DirectoryManager(_rootCoverArtPath);
|
|
||||||
var defaultExtension = ".png";
|
|
||||||
dirMgr.CreateDirectory(song);
|
|
||||||
|
|
||||||
var segment = cover.GenerateFilename(0);
|
|
||||||
var imagePath = dirMgr.SongDirectory + segment + defaultExtension;
|
|
||||||
|
|
||||||
cover.ImagePath = imagePath;
|
|
||||||
|
|
||||||
using (var fileStream = new FileStream(imagePath, FileMode.Create))
|
|
||||||
{
|
|
||||||
data.CopyTo(fileStream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Error(ex.Message, "An error occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
return cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CoverArt GetCoverArt(Song song)
|
|
||||||
{
|
|
||||||
return _coverArtContext.CoverArtImages.FirstOrDefault(cov => cov.SongTitle.Equals(song.Title));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Initialize()
|
|
||||||
{
|
|
||||||
_coverArtContext = new CoverArtContext(_connectionString);
|
|
||||||
|
|
||||||
if (System.IO.File.Exists(DirectoryPaths.CoverArtPath))
|
|
||||||
_stockCoverArt = File.ReadAllBytes(DirectoryPaths.CoverArtPath);
|
|
||||||
|
|
||||||
if (!File.Exists(_rootCoverArtPath + "CoverArt.png"))
|
|
||||||
{
|
|
||||||
File.WriteAllBytes(_rootCoverArtPath + "CoverArt.png",
|
|
||||||
_stockCoverArt);
|
|
||||||
Console.WriteLine("Copied Stock Cover Art");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,232 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Types;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
|
||||||
{
|
|
||||||
// NOTE: Do not use metadata for the song's metadata
|
|
||||||
public class DirectoryManager : BaseManager
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private Song _song;
|
|
||||||
private string _rootSongDirectory;
|
|
||||||
private string _songDirectory;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
public string SongDirectory
|
|
||||||
{
|
|
||||||
get => _songDirectory;
|
|
||||||
set => _songDirectory = value;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public DirectoryManager(IConfiguration config, Song song)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_song = song;
|
|
||||||
Initialize();
|
|
||||||
}
|
|
||||||
public DirectoryManager(IConfiguration config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
Initialize();
|
|
||||||
}
|
|
||||||
public DirectoryManager(string rootDirectory)
|
|
||||||
{
|
|
||||||
_rootSongDirectory = rootDirectory;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public void CreateDirectory()
|
|
||||||
{
|
|
||||||
CreateDirectory(_song);
|
|
||||||
}
|
|
||||||
public void CreateDirectory(Song song)
|
|
||||||
{
|
|
||||||
_song = song;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_songDirectory = AlbumDirectory();
|
|
||||||
|
|
||||||
if (!Directory.Exists(_songDirectory))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(_songDirectory);
|
|
||||||
Console.WriteLine($"The directory has been created");
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine($"The song will be saved in the following" +
|
|
||||||
$" directory: {_songDirectory}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public void DeleteEmptyDirectories()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var albumDirectory = AlbumDirectory();
|
|
||||||
var artistDirectory = ArtistDirectory();
|
|
||||||
if (IsDirectoryEmpty(albumDirectory))
|
|
||||||
{
|
|
||||||
Directory.Delete(albumDirectory);
|
|
||||||
Console.WriteLine($"directory {albumDirectory} deleted");
|
|
||||||
}
|
|
||||||
if (IsDirectoryEmpty(artistDirectory))
|
|
||||||
{
|
|
||||||
Directory.Delete(artistDirectory);
|
|
||||||
Console.WriteLine($"directory {artistDirectory} deleted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exMsg = ex.Message;
|
|
||||||
Console.WriteLine($"An error occurred {exMsg}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public void DeleteEmptyDirectories(Song song)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var albumDirectory = AlbumDirectory(song);
|
|
||||||
var artistDirectory = ArtistDirectory(song);
|
|
||||||
|
|
||||||
if (IsDirectoryEmpty(albumDirectory))
|
|
||||||
{
|
|
||||||
Directory.Delete(albumDirectory);
|
|
||||||
_logger.Info("Album directory deleted");
|
|
||||||
}
|
|
||||||
if (IsDirectoryEmpty(artistDirectory))
|
|
||||||
{
|
|
||||||
Directory.Delete(artistDirectory);
|
|
||||||
_logger.Info("Artist directory deleted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string RetrieveAlbumPath(Song song)
|
|
||||||
{
|
|
||||||
_logger.Info("Retrieving album song path");
|
|
||||||
|
|
||||||
var albumPath = string.Empty;
|
|
||||||
albumPath = AlbumDirectory(song);
|
|
||||||
|
|
||||||
return albumPath;
|
|
||||||
}
|
|
||||||
public string RetrieveArtistPath(Song song)
|
|
||||||
{
|
|
||||||
_logger.Info("Retrieving artist path");
|
|
||||||
|
|
||||||
var artistPath = string.Empty;
|
|
||||||
artistPath = ArtistDirectory(song);
|
|
||||||
|
|
||||||
return artistPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GenerateSongPath(Song song)
|
|
||||||
{
|
|
||||||
_logger.Info("Generating song path");
|
|
||||||
|
|
||||||
var songPath = string.Empty;
|
|
||||||
var artistPath = ArtistDirectory(song);
|
|
||||||
var albumPath = AlbumDirectory(song);
|
|
||||||
|
|
||||||
if (!Directory.Exists(artistPath))
|
|
||||||
{
|
|
||||||
_logger.Info("Artist path does not exist");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(artistPath);
|
|
||||||
|
|
||||||
_logger.Info("Creating artist path");
|
|
||||||
}
|
|
||||||
if (!Directory.Exists(albumPath))
|
|
||||||
{
|
|
||||||
_logger.Info("Album path does not exist");
|
|
||||||
|
|
||||||
Directory.CreateDirectory(albumPath);
|
|
||||||
|
|
||||||
_logger.Info("Created album path");
|
|
||||||
}
|
|
||||||
|
|
||||||
songPath = albumPath;
|
|
||||||
|
|
||||||
return songPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Initialize(DirectoryType dirTypes = DirectoryType.Music)
|
|
||||||
{
|
|
||||||
switch (dirTypes)
|
|
||||||
{
|
|
||||||
case DirectoryType.Music:
|
|
||||||
_rootSongDirectory = _config.GetValue<string>("RootMusicPath");
|
|
||||||
break;
|
|
||||||
case DirectoryType.CoverArt:
|
|
||||||
_rootSongDirectory = _config.GetValue<string>("CoverArtPath");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsDirectoryEmpty(string path)
|
|
||||||
{
|
|
||||||
return !Directory.EnumerateFileSystemEntries(path).Any();
|
|
||||||
}
|
|
||||||
|
|
||||||
private string AlbumDirectory()
|
|
||||||
{
|
|
||||||
return AlbumDirectory(_song);
|
|
||||||
}
|
|
||||||
private string AlbumDirectory(Song song)
|
|
||||||
{
|
|
||||||
var directory = ArtistDirectory(song);
|
|
||||||
var segment = SerializeValue(song.AlbumTitle);
|
|
||||||
directory += $@"{segment}/";
|
|
||||||
Console.WriteLine($"Album directory {directory}");
|
|
||||||
|
|
||||||
return directory;
|
|
||||||
}
|
|
||||||
private string ArtistDirectory()
|
|
||||||
{
|
|
||||||
return ArtistDirectory(_song);
|
|
||||||
}
|
|
||||||
private string ArtistDirectory(Song song)
|
|
||||||
{
|
|
||||||
var directory = _rootSongDirectory;
|
|
||||||
var segment = SerializeValue(song.Artist);
|
|
||||||
directory += $@"{segment}/";
|
|
||||||
Console.WriteLine($"Artist directory {directory}");
|
|
||||||
|
|
||||||
return directory;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string SerializeValue(string value)
|
|
||||||
{
|
|
||||||
const int length = 15;
|
|
||||||
const string chars = "ABCDEF0123456789";
|
|
||||||
var random = new Random();
|
|
||||||
var output = new string(Enumerable.Repeat(chars, length).Select(s =>
|
|
||||||
s[random.Next(s.Length)]).ToArray());
|
|
||||||
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
|
||||||
{
|
|
||||||
public class GenreManager : BaseManager
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private GenreContext _genreContext;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public GenreManager(IConfiguration config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
_genreContext = new GenreContext(_connectionString);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public void SaveGenreToDatabase(ref Song song)
|
|
||||||
{
|
|
||||||
_logger.Info("Starting process to save the genre record of the song to the database");
|
|
||||||
|
|
||||||
var genre = new Genre
|
|
||||||
{
|
|
||||||
GenreName = song.Genre,
|
|
||||||
SongCount = 1
|
|
||||||
};
|
|
||||||
|
|
||||||
var genreName = song.Genre;
|
|
||||||
var genreRetrieved = _genreContext.Genres.FirstOrDefault(gnr => gnr.GenreName.Equals(genreName));
|
|
||||||
|
|
||||||
if (genreRetrieved == null)
|
|
||||||
{
|
|
||||||
_genreContext.Add(genre);
|
|
||||||
_genreContext.SaveChanges();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
genre.GenreID = genreRetrieved.GenreID;
|
|
||||||
}
|
|
||||||
|
|
||||||
song.GenreID = genre.GenreID;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Genre UpdateGenreInDatabase(Song oldSongRecord, Song newSongRecord)
|
|
||||||
{
|
|
||||||
var oldGenreRecord = _genreContext.Genres.FirstOrDefault(gnr => gnr.GenreName.Equals(oldSongRecord.Genre));
|
|
||||||
var oldGenreName = oldGenreRecord.GenreName;
|
|
||||||
var newGenreName = newSongRecord.Genre;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(newGenreName) || oldGenreName.Equals(newGenreName))
|
|
||||||
{
|
|
||||||
_logger.Info("No change to the song's Genre");
|
|
||||||
return oldGenreRecord;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Info("Change to the song's genre found");
|
|
||||||
|
|
||||||
if (oldGenreRecord.SongCount <= 1)
|
|
||||||
{
|
|
||||||
_logger.Info("Deleting genre record");
|
|
||||||
|
|
||||||
_genreContext.Remove(oldGenreRecord);
|
|
||||||
_genreContext.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!(_genreContext.Genres.FirstOrDefault(gnr => gnr.GenreName.Equals(oldSongRecord.Genre)) != null))
|
|
||||||
{
|
|
||||||
_logger.Info("Creating new genre record");
|
|
||||||
|
|
||||||
var newGenreRecord = new Genre
|
|
||||||
{
|
|
||||||
GenreName = newGenreName
|
|
||||||
};
|
|
||||||
|
|
||||||
_genreContext.Add(newGenreRecord);
|
|
||||||
_genreContext.SaveChanges();
|
|
||||||
|
|
||||||
return newGenreRecord;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.Info("Updating existing genre record");
|
|
||||||
|
|
||||||
var existingGenreRecord = _genreContext.Genres.FirstOrDefault(gnr => gnr.GenreName.Equals(oldGenreRecord.GenreName));
|
|
||||||
|
|
||||||
_genreContext.Update(existingGenreRecord);
|
|
||||||
_genreContext.SaveChanges();
|
|
||||||
|
|
||||||
return existingGenreRecord;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void DeleteGenreFromDatabase(Song song)
|
|
||||||
{
|
|
||||||
if (!(_genreContext.Genres.FirstOrDefault(gnr => gnr.GenreName.Equals(song.Genre)) != null))
|
|
||||||
{
|
|
||||||
_logger.Info("Cannot delete the genre record because it does not exist");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var genre = _genreContext.Genres.FirstOrDefault(gnr => gnr.GenreName.Equals(song.Genre));
|
|
||||||
|
|
||||||
if (SongsInGenre(genre) <= 1)
|
|
||||||
{
|
|
||||||
_genreContext.Remove(genre);
|
|
||||||
_genreContext.SaveChanges();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private int SongsInGenre(Genre genre)
|
|
||||||
{
|
|
||||||
var sngContext = new SongContext(_connectionString);
|
|
||||||
var songs = sngContext.Songs.Where(sng => sng.GenreID == genre.GenreID).ToList();
|
|
||||||
|
|
||||||
return songs.Count;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,507 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Data;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using NLog;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
|
||||||
{
|
|
||||||
public class SongManager : BaseManager
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private string _tempDirectoryRoot;
|
|
||||||
private string _archiveDirectoryRoot;
|
|
||||||
private string _compressedSongFilename;
|
|
||||||
private string _message;
|
|
||||||
private SongContext _songContext;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
public string ArchiveDirectoryRoot
|
|
||||||
{
|
|
||||||
get => _archiveDirectoryRoot;
|
|
||||||
set => _archiveDirectoryRoot = value;
|
|
||||||
}
|
|
||||||
public string CompressedSongFilename
|
|
||||||
{
|
|
||||||
get => _compressedSongFilename;
|
|
||||||
set => _compressedSongFilename = value;
|
|
||||||
}
|
|
||||||
public string Message
|
|
||||||
{
|
|
||||||
get => _message;
|
|
||||||
set => _message = value;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public SongManager(IConfiguration config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
Initialize();
|
|
||||||
}
|
|
||||||
public SongManager(IConfiguration config, string tempDirectoryRoot)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_tempDirectoryRoot = tempDirectoryRoot;
|
|
||||||
Initialize();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public SongResult UpdateSong(Song song)
|
|
||||||
{
|
|
||||||
var result = new SongResult();
|
|
||||||
if (!DoesSongExist(song))
|
|
||||||
{
|
|
||||||
result.SongTitle = song.Title;
|
|
||||||
result.Message = "Song does not exist";
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var oldSongRecord = _songContext.RetrieveRecord(song);
|
|
||||||
song.Filename = oldSongRecord.Filename;
|
|
||||||
song.SongDirectory = oldSongRecord.SongDirectory;
|
|
||||||
|
|
||||||
MetadataRetriever updateMetadata = new MetadataRetriever();
|
|
||||||
updateMetadata.UpdateMetadata(song, oldSongRecord);
|
|
||||||
|
|
||||||
var updatedSong = updateMetadata.UpdatedSongRecord;
|
|
||||||
|
|
||||||
var albMgr = new AlbumManager(_config);
|
|
||||||
var gnrMgr = new GenreManager(_config);
|
|
||||||
var artMgr = new ArtistManager(_config);
|
|
||||||
var updatedAlbum = albMgr.UpdateAlbumInDatabase(oldSongRecord, updatedSong);
|
|
||||||
oldSongRecord.AlbumID = updatedAlbum.AlbumID;
|
|
||||||
|
|
||||||
var updatedArtist = artMgr.UpdateArtistInDatabase(oldSongRecord, updatedSong);
|
|
||||||
oldSongRecord.ArtistID = updatedArtist.ArtistID;
|
|
||||||
|
|
||||||
var updatedGenre = gnrMgr.UpdateGenreInDatabase(oldSongRecord, updatedSong);
|
|
||||||
oldSongRecord.GenreID = updatedGenre.GenreID;
|
|
||||||
|
|
||||||
UpdateSongInDatabase(ref oldSongRecord, ref updatedSong, ref result);
|
|
||||||
|
|
||||||
DeleteEmptyDirectories(ref oldSongRecord, ref updatedSong);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
|
|
||||||
result.Message = $"An error occurred: {msg}";
|
|
||||||
result.SongTitle = song.Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DeleteSongFromFileSystem(Song songMetaData)
|
|
||||||
{
|
|
||||||
bool successful = false;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var songPath = songMetaData.SongPath();
|
|
||||||
System.IO.File.Delete(songPath);
|
|
||||||
successful = true;
|
|
||||||
DirectoryManager dirMgr = new DirectoryManager(_config, songMetaData);
|
|
||||||
dirMgr.DeleteEmptyDirectories();
|
|
||||||
Console.WriteLine("Song successfully deleted");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exMsg = ex.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
return successful;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DoesSongExist(Song song)
|
|
||||||
{
|
|
||||||
if (!_songContext.DoesRecordExist(song))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
public void DeleteSong(Song song)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (DeleteSongFromFilesystem(song))
|
|
||||||
{
|
|
||||||
_logger.Error("Failed to delete the song");
|
|
||||||
|
|
||||||
throw new Exception("Failed to delete the song");
|
|
||||||
}
|
|
||||||
_logger.Info("Song deleted from the filesystem");
|
|
||||||
|
|
||||||
var coverMgr = new CoverArtManager(_config);
|
|
||||||
|
|
||||||
var coverArt = coverMgr.GetCoverArt(song);
|
|
||||||
coverMgr.DeleteCoverArt(coverArt);
|
|
||||||
|
|
||||||
coverMgr.DeleteCoverArtFromDatabase(coverArt);
|
|
||||||
DeleteSongFromDatabase(song);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public async Task SaveSongToFileSystem(IFormFile songFile)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_logger.Info("Starting the process of saving the song to the filesystem");
|
|
||||||
|
|
||||||
var song = await SaveSongTemp(songFile);
|
|
||||||
|
|
||||||
DirectoryManager dirMgr = new DirectoryManager(_config, song);
|
|
||||||
dirMgr.CreateDirectory();
|
|
||||||
|
|
||||||
var tempPath = song.SongPath();
|
|
||||||
song.Filename = song.GenerateFilename(1);
|
|
||||||
var filePath = $"{dirMgr.SongDirectory}{song.Filename}";
|
|
||||||
|
|
||||||
_logger.Info($"Absolute song path: {filePath}");
|
|
||||||
|
|
||||||
|
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
|
||||||
using (var fileStream = new FileStream(filePath, FileMode.Create))
|
|
||||||
{
|
|
||||||
var songBytes = System.IO.File.ReadAllBytes(tempPath);
|
|
||||||
|
|
||||||
_logger.Info("Saving song to the filesystem");
|
|
||||||
fileStream.Write(songBytes, 0, songBytes.Count());
|
|
||||||
|
|
||||||
System.IO.File.Delete(tempPath);
|
|
||||||
_logger.Info("Deleting temp file");
|
|
||||||
|
|
||||||
_logger.Info("Song successfully saved to filesystem");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
song.SongDirectory = dirMgr.SongDirectory;
|
|
||||||
|
|
||||||
var coverMgr = new CoverArtManager(_config);
|
|
||||||
var coverArt = coverMgr.SaveCoverArt(song);
|
|
||||||
|
|
||||||
coverMgr.SaveCoverArtToDatabase(ref song, ref coverArt);
|
|
||||||
SaveSongToDatabase(song);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void SaveSongToFileSystem(IFormFile songFile, IFormFile coverArtData, Song song)
|
|
||||||
{
|
|
||||||
song.SongDirectory = _tempDirectoryRoot;
|
|
||||||
song.DateCreated = DateTime.Now;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(song.Filename))
|
|
||||||
{
|
|
||||||
song.Filename = song.GenerateFilename(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Info($"Temporary directory: {_tempDirectoryRoot}");
|
|
||||||
|
|
||||||
var tempPath = song.SongPath();
|
|
||||||
|
|
||||||
_logger.Info("Temporary song path: {0}", tempPath);
|
|
||||||
|
|
||||||
using (var filestream = new FileStream(tempPath, FileMode.Create))
|
|
||||||
{
|
|
||||||
_logger.Info("Saving song to temporary directory");
|
|
||||||
songFile.CopyTo(filestream);
|
|
||||||
}
|
|
||||||
|
|
||||||
var coverMgr = new CoverArtManager(_config);
|
|
||||||
var meta = new MetadataRetriever();
|
|
||||||
var coverArt = coverMgr.SaveCoverArt(coverArtData, song);
|
|
||||||
meta.UpdateCoverArt(song, coverArt);
|
|
||||||
song.Duration = meta.RetrieveSongDuration(song.SongPath());
|
|
||||||
|
|
||||||
meta.UpdateMetadata(song, song);
|
|
||||||
|
|
||||||
|
|
||||||
DirectoryManager dirMgr = new DirectoryManager(_config, song);
|
|
||||||
dirMgr.CreateDirectory();
|
|
||||||
|
|
||||||
song.SongDirectory = dirMgr.SongDirectory;
|
|
||||||
|
|
||||||
var filePath = song.SongPath();
|
|
||||||
_logger.Info($"Absolute song path: {filePath}");
|
|
||||||
|
|
||||||
using (var fileStream = new FileStream(filePath, FileMode.Create))
|
|
||||||
{
|
|
||||||
var songBytes = System.IO.File.ReadAllBytes(tempPath);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (System.IO.File.Exists(filePath) && System.IO.File.Exists(tempPath) && fileStream.Length > 0)
|
|
||||||
{
|
|
||||||
System.IO.File.Delete(tempPath);
|
|
||||||
_logger.Info("Deleted temp song from filesystem: {0}", tempPath);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fileStream.Write(songBytes, 0, songBytes.Count());
|
|
||||||
_logger.Info("Saved song to filesystem: {0}", filePath);
|
|
||||||
|
|
||||||
System.IO.File.Delete(tempPath);
|
|
||||||
_logger.Info("Deleted temp song from filesystem: {0}", tempPath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error($"An error occurred: {msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Info("Song successfully saved to filesystem");
|
|
||||||
}
|
|
||||||
|
|
||||||
coverMgr.SaveCoverArtToDatabase(ref song, ref coverArt);
|
|
||||||
SaveSongToDatabase(song);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public async Task<SongData> RetrieveSong(Song songMetaData)
|
|
||||||
{
|
|
||||||
var song = new SongData();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine("Fetching song from filesystem");
|
|
||||||
song = await RetrieveSongFromFileSystem(songMetaData);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exMsg = ex.Message;
|
|
||||||
Console.WriteLine($"An error occurred: {exMsg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return song;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private async Task<SongData> RetrieveSongFromFileSystem(Song details)
|
|
||||||
{
|
|
||||||
byte[] uncompressedSong = await System.IO.File.ReadAllBytesAsync(details.SongPath());
|
|
||||||
|
|
||||||
return new SongData
|
|
||||||
{
|
|
||||||
Data = uncompressedSong
|
|
||||||
};
|
|
||||||
}
|
|
||||||
private async Task<Song> SaveSongTemp(IFormFile songFile)
|
|
||||||
{
|
|
||||||
var song = new Song();
|
|
||||||
_logger.Info("Assigning song filename");
|
|
||||||
song.SongDirectory = _tempDirectoryRoot;
|
|
||||||
var filename = song.GenerateFilename(1);
|
|
||||||
song.Filename = filename;
|
|
||||||
|
|
||||||
using (var filestream = new FileStream(song.SongPath(), FileMode.Create))
|
|
||||||
{
|
|
||||||
_logger.Info("Saving temp song: {0}", song.SongPath());
|
|
||||||
await songFile.CopyToAsync(filestream);
|
|
||||||
}
|
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
|
||||||
MetadataRetriever meta = new MetadataRetriever();
|
|
||||||
song = meta.RetrieveMetaData(song.SongPath());
|
|
||||||
});
|
|
||||||
|
|
||||||
song.SongDirectory = _tempDirectoryRoot;
|
|
||||||
song.DateCreated = DateTime.Now;
|
|
||||||
song.Filename = filename;
|
|
||||||
|
|
||||||
return song;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private bool SongRecordChanged(Song currentSong, Song songUpdates)
|
|
||||||
{
|
|
||||||
var currentTitle = currentSong.Title;
|
|
||||||
var currentArtist = currentSong.Artist;
|
|
||||||
var currentAlbum = currentSong.AlbumTitle;
|
|
||||||
var currentGenre = currentSong.Genre;
|
|
||||||
var currentYear = currentSong.Year;
|
|
||||||
|
|
||||||
if (!currentTitle.Equals(songUpdates.Title) || !currentArtist.Equals(songUpdates.Artist) ||
|
|
||||||
!currentAlbum.Equals(songUpdates.AlbumTitle) ||
|
|
||||||
!currentGenre.Equals(songUpdates.Genre) || currentYear != songUpdates.Year)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteEmptyDirectories(ref Song oldSong, ref Song updatedSong)
|
|
||||||
{
|
|
||||||
DirectoryManager mgr = new DirectoryManager(_config);
|
|
||||||
|
|
||||||
_logger.Info("Checking to see if there are any directories to delete");
|
|
||||||
mgr.DeleteEmptyDirectories(oldSong);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Initialize()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
_songContext = new SongContext(_connectionString);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error Occurred: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private void SaveSongToDatabase(Song song)
|
|
||||||
{
|
|
||||||
_logger.Info("Starting process to save the song to the database");
|
|
||||||
|
|
||||||
var albumMgr = new AlbumManager(_config);
|
|
||||||
var artistMgr = new ArtistManager(_config);
|
|
||||||
var genreMgr = new GenreManager(_config);
|
|
||||||
albumMgr.SaveAlbumToDatabase(ref song);
|
|
||||||
artistMgr.SaveArtistToDatabase(ref song);
|
|
||||||
genreMgr.SaveGenreToDatabase(ref song);
|
|
||||||
|
|
||||||
var info = "Saving Song to DB";
|
|
||||||
_logger.Info(info);
|
|
||||||
|
|
||||||
_songContext.Add(song);
|
|
||||||
_songContext.SaveChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private bool DeleteSongFromFilesystem(Song song)
|
|
||||||
{
|
|
||||||
var songPath = song.SongPath();
|
|
||||||
|
|
||||||
_logger.Info("Deleting song from the filesystem");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
System.IO.File.Delete(songPath);
|
|
||||||
}
|
|
||||||
catch(Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred when attempting to delete the song from the filesystem");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return DoesSongExistOnFilesystem(song);
|
|
||||||
}
|
|
||||||
private bool DoesSongExistOnFilesystem(Song song)
|
|
||||||
{
|
|
||||||
if (!System.IO.File.Exists(song.SongPath()))
|
|
||||||
{
|
|
||||||
_logger.Info("Song does not exist on the filesystem");
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Info("Song exists on the filesystem");
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void UpdateSongInDatabase(ref Song oldSongRecord, ref Song newSongRecord, ref SongResult result)
|
|
||||||
{
|
|
||||||
var updatedSongRecord = oldSongRecord;
|
|
||||||
|
|
||||||
var songContext = new SongContext(_connectionString);
|
|
||||||
|
|
||||||
if (!SongRecordChanged(oldSongRecord, newSongRecord))
|
|
||||||
{
|
|
||||||
_logger.Info("No change to the song record");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.Info("Changes to song record found");
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(newSongRecord.Title))
|
|
||||||
updatedSongRecord.Title = newSongRecord.Title;
|
|
||||||
if (!string.IsNullOrEmpty(newSongRecord.AlbumTitle))
|
|
||||||
{
|
|
||||||
updatedSongRecord.AlbumTitle = newSongRecord.AlbumTitle;
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrEmpty(newSongRecord.Artist))
|
|
||||||
{
|
|
||||||
updatedSongRecord.Artist = newSongRecord.Artist;
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrEmpty(newSongRecord.Genre))
|
|
||||||
{
|
|
||||||
updatedSongRecord.Genre = newSongRecord.Genre;
|
|
||||||
Console.WriteLine("Genre changed");
|
|
||||||
Console.WriteLine($"{updatedSongRecord.Genre} {newSongRecord.Genre}");
|
|
||||||
}
|
|
||||||
if (newSongRecord.Year != null || newSongRecord.Year > 0)
|
|
||||||
updatedSongRecord.Year = newSongRecord.Year;
|
|
||||||
|
|
||||||
_logger.Info("Applied changes to song record");
|
|
||||||
|
|
||||||
|
|
||||||
_logger.Info("Saving song metadata to the database");
|
|
||||||
|
|
||||||
songContext.Update(updatedSongRecord);
|
|
||||||
songContext.SaveChanges();
|
|
||||||
|
|
||||||
newSongRecord = updatedSongRecord;
|
|
||||||
|
|
||||||
result.Message = "Successfully updated song";
|
|
||||||
result.SongTitle = updatedSongRecord.Title;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DeleteSongFromDatabase(Song song)
|
|
||||||
{
|
|
||||||
_logger.Info("Starting process to delete records related to the song from the database");
|
|
||||||
|
|
||||||
var albumMgr = new AlbumManager(_config);
|
|
||||||
var artistMgr = new ArtistManager(_config);
|
|
||||||
var genreMgr = new GenreManager(_config);
|
|
||||||
albumMgr.DeleteAlbumFromDatabase(song);
|
|
||||||
artistMgr.DeleteArtistFromDatabase(song);
|
|
||||||
genreMgr.DeleteGenreFromDatabase(song);
|
|
||||||
|
|
||||||
var sngContext = new SongContext(_connectionString);
|
|
||||||
sngContext.Songs.Remove(song);
|
|
||||||
sngContext.SaveChanges();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,410 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using System.IdentityModel.Tokens.Jwt;
|
|
||||||
using System.Security.Claims;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
using JWT;
|
|
||||||
using JWT.Serializers;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Org.BouncyCastle.Crypto;
|
|
||||||
using Org.BouncyCastle.Crypto.Parameters;
|
|
||||||
using Org.BouncyCastle.OpenSsl;
|
|
||||||
using Org.BouncyCastle.Security;
|
|
||||||
|
|
||||||
using RestSharp;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Managers
|
|
||||||
{
|
|
||||||
#region Classes
|
|
||||||
public class TokenManager : BaseManager
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private string _clientId;
|
|
||||||
private string _clientSecret;
|
|
||||||
private string _privateKeyPath;
|
|
||||||
private string _privateKey;
|
|
||||||
private string _publicKeyPath;
|
|
||||||
private string _publicKey;
|
|
||||||
private string _audience;
|
|
||||||
private string _grantType;
|
|
||||||
private string _url;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public TokenManager(IConfiguration config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
InitializeValues();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public LoginResult RetrieveLoginResult(User user)
|
|
||||||
{
|
|
||||||
_logger.Info("Preparing Auth0 API request");
|
|
||||||
|
|
||||||
var client = new RestClient(_url);
|
|
||||||
var request = new RestRequest("oauth/token", Method.POST);
|
|
||||||
var tokenRequest = RetrieveTokenRequest();
|
|
||||||
|
|
||||||
_logger.Info("Serializing token object into JSON");
|
|
||||||
var tokenObject = JsonConvert.SerializeObject(tokenRequest);
|
|
||||||
|
|
||||||
request.AddParameter("application/json; charset=utf-8",
|
|
||||||
tokenObject, ParameterType.RequestBody);
|
|
||||||
|
|
||||||
request.RequestFormat = DataFormat.Json;
|
|
||||||
|
|
||||||
_logger.Info("Sending request");
|
|
||||||
IRestResponse response = client.Execute(request);
|
|
||||||
_logger.Info("Response received");
|
|
||||||
|
|
||||||
|
|
||||||
_logger.Info("Deserializing response");
|
|
||||||
var tokenResult = JsonConvert
|
|
||||||
.DeserializeObject<TokenTierOne>(response.Content);
|
|
||||||
_logger.Info("Response deserialized");
|
|
||||||
|
|
||||||
return new LoginResult
|
|
||||||
{
|
|
||||||
UserID = user.UserID, Username = user.Username, Token = tokenResult.AccessToken,
|
|
||||||
TokenType = tokenResult.TokenType, Expiration = tokenResult.Expiration,
|
|
||||||
Message = "Successfully retrieved token"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public LoginResult LogIn(User user)
|
|
||||||
{
|
|
||||||
var tokenResult = new TokenTierOne();
|
|
||||||
tokenResult.TokenType = "Jwt";
|
|
||||||
|
|
||||||
var privateKey = ReadKeyContent(_privateKeyPath).Result;
|
|
||||||
var publicKey = ReadKeyContent(_publicKeyPath).Result;
|
|
||||||
|
|
||||||
var payload = Payload();
|
|
||||||
|
|
||||||
var token = CreateToken(payload, privateKey);
|
|
||||||
tokenResult.AccessToken = token;
|
|
||||||
|
|
||||||
var expClaim = payload.FirstOrDefault(cl =>
|
|
||||||
{
|
|
||||||
return cl.Type.Equals("exp");
|
|
||||||
});
|
|
||||||
|
|
||||||
tokenResult.Expiration = System.Convert.ToInt32(expClaim.Value);
|
|
||||||
|
|
||||||
return new LoginResult
|
|
||||||
{
|
|
||||||
UserID = user.UserID, Username = user.Username, Token = tokenResult.AccessToken,
|
|
||||||
TokenType = tokenResult.TokenType, Expiration = tokenResult.Expiration,
|
|
||||||
Message = "Successfully retrieved token"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public LoginResult LoginSymmetric(User user)
|
|
||||||
{
|
|
||||||
var tokenResult = new TokenTierOne();
|
|
||||||
tokenResult.TokenType = "Jwt";
|
|
||||||
|
|
||||||
var payload = Payload();
|
|
||||||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["JWT:Secret"]));
|
|
||||||
var signIn = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
|
||||||
var token = new JwtSecurityToken(
|
|
||||||
_config["JWT:Issuer"],
|
|
||||||
_config["JWT:Audience"],
|
|
||||||
payload,
|
|
||||||
expires: DateTime.UtcNow.AddMinutes(30),
|
|
||||||
signingCredentials: signIn);
|
|
||||||
|
|
||||||
tokenResult.AccessToken = new JwtSecurityTokenHandler().WriteToken(token);
|
|
||||||
|
|
||||||
var expClaim = payload.FirstOrDefault(cl =>
|
|
||||||
{
|
|
||||||
return cl.Type.Equals("exp");
|
|
||||||
});
|
|
||||||
|
|
||||||
var expiredDate = DateTime.Parse(expClaim.Value);
|
|
||||||
var exp = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds);
|
|
||||||
tokenResult.Expiration = Convert.ToInt32(exp);
|
|
||||||
|
|
||||||
return new LoginResult
|
|
||||||
{
|
|
||||||
UserID = user.UserID, Username = user.Username, Token = tokenResult.AccessToken,
|
|
||||||
TokenType = tokenResult.TokenType, Expiration = tokenResult.Expiration,
|
|
||||||
Message = "Successfully retrieved token"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IsTokenValid(string scope, string accessToken)
|
|
||||||
{
|
|
||||||
var result = false;
|
|
||||||
var token = DecodeToken(accessToken);
|
|
||||||
|
|
||||||
if (token == null || token.Erroneous())
|
|
||||||
{
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = (!token.TokenExpired() && token.ContainsScope(scope)) ? true : false;
|
|
||||||
|
|
||||||
// What would make a token valid?
|
|
||||||
// 1. The expiration date must be before the current date
|
|
||||||
// 2. The desired scope must be part of the scopes within the access token
|
|
||||||
// 3. Must be able to be decoded
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Token? DecodeToken(string accessToken)
|
|
||||||
{
|
|
||||||
var rsaParams = GetRSAPublic(_publicKey);
|
|
||||||
Token tok = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (var rsa = new RSACryptoServiceProvider())
|
|
||||||
{
|
|
||||||
rsa.ImportParameters(rsaParams);
|
|
||||||
|
|
||||||
IJsonSerializer serializer = new JsonNetSerializer();
|
|
||||||
IDateTimeProvider provider = new UtcDateTimeProvider();
|
|
||||||
IJwtValidator validator = new JwtValidator(serializer, provider);
|
|
||||||
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
|
|
||||||
var algorithm = new JWT.Algorithms.RS256Algorithm(rsa);
|
|
||||||
IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder, algorithm);
|
|
||||||
|
|
||||||
var json = decoder.Decode(accessToken);
|
|
||||||
tok = JsonConvert.DeserializeObject<Token>(json);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.Error("An error occurred: {0}", ex.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return tok;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private string AllScopes()
|
|
||||||
{
|
|
||||||
var allScopes = new List<String>()
|
|
||||||
{
|
|
||||||
"download:songs",
|
|
||||||
"read:song_details",
|
|
||||||
"upload:songs",
|
|
||||||
"delete:songs",
|
|
||||||
"read:albums",
|
|
||||||
"read:artists",
|
|
||||||
"update:songs",
|
|
||||||
"stream:songs",
|
|
||||||
"read:genre",
|
|
||||||
"read:year",
|
|
||||||
"download:cover_art"
|
|
||||||
};
|
|
||||||
|
|
||||||
var scopes = string.Empty;
|
|
||||||
|
|
||||||
for (var i = 0; i < allScopes.Count; i++)
|
|
||||||
{
|
|
||||||
if (i == allScopes.Count - 1)
|
|
||||||
{
|
|
||||||
scopes += allScopes[i];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
scopes += allScopes[i] + " ";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return scopes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Claim> Payload()
|
|
||||||
{
|
|
||||||
var expLimit = 30;
|
|
||||||
var currentDate = DateTime.Now;
|
|
||||||
var expiredDate = currentDate.AddMinutes(expLimit);
|
|
||||||
var issued = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds);
|
|
||||||
var expires = Math.Floor((expiredDate - DateTime.UnixEpoch).TotalSeconds);
|
|
||||||
var issuer = "https://soaricarus.auth0.com";
|
|
||||||
issuer = "http://localhost:5002";
|
|
||||||
var audience = "https://icarus/api";
|
|
||||||
audience = "http://localhost:5002";
|
|
||||||
var subject = _config["JWT:Subject"];
|
|
||||||
|
|
||||||
var claim = new List<System.Security.Claims.Claim>()
|
|
||||||
{
|
|
||||||
new System.Security.Claims.Claim("scope", AllScopes(), "string"),
|
|
||||||
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Exp, expiredDate.ToString()),
|
|
||||||
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Aud, audience),
|
|
||||||
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iss, issuer),
|
|
||||||
new Claim(JwtRegisteredClaimNames.Sub, subject),
|
|
||||||
new System.Security.Claims.Claim(JwtRegisteredClaimNames.Iat, currentDate.ToString())
|
|
||||||
};
|
|
||||||
|
|
||||||
return claim;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string CreateToken(List<Claim> claims, string privateKey)
|
|
||||||
{
|
|
||||||
var token = string.Empty;
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(privateKey))
|
|
||||||
{
|
|
||||||
privateKey = ReadKeyContent(_privateKeyPath).Result;
|
|
||||||
}
|
|
||||||
|
|
||||||
RSAParameters rsaParams;
|
|
||||||
using (var tr = new System.IO.StringReader(privateKey))
|
|
||||||
{
|
|
||||||
var pemReader = new PemReader(tr);
|
|
||||||
var keyPair = pemReader.ReadObject() as AsymmetricCipherKeyPair;
|
|
||||||
if (keyPair == null)
|
|
||||||
{
|
|
||||||
throw new Exception("Could not read RSA private key");
|
|
||||||
}
|
|
||||||
var privateRsaParams = keyPair.Private as RsaPrivateCrtKeyParameters;
|
|
||||||
rsaParams = DotNetUtilities.ToRSAParameters(privateRsaParams);
|
|
||||||
}
|
|
||||||
|
|
||||||
using (RSACryptoServiceProvider rsa = new RSACryptoServiceProvider())
|
|
||||||
{
|
|
||||||
var rsaParamsPublic = GetRSAPublic(ReadKeyContent(_publicKeyPath).Result);
|
|
||||||
var rsaPublic = new RSACryptoServiceProvider();
|
|
||||||
|
|
||||||
rsa.ImportParameters(rsaParams);
|
|
||||||
rsaPublic.ImportParameters(rsaParamsPublic);
|
|
||||||
|
|
||||||
Dictionary<string, object> payload = new Dictionary<string, object>();
|
|
||||||
|
|
||||||
foreach (var claim in claims)
|
|
||||||
{
|
|
||||||
var type = claim.Type;
|
|
||||||
var val = Int32.TryParse(claim.Value, out _);
|
|
||||||
|
|
||||||
if (val)
|
|
||||||
{
|
|
||||||
payload.Add(type, Convert.ToInt32(claim.Value));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
payload.Add(type, claim.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
var algorithm = new JWT.Algorithms.RS256Algorithm(rsaPublic, rsa);
|
|
||||||
IJsonSerializer serializer = new JsonNetSerializer();
|
|
||||||
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
|
|
||||||
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
|
|
||||||
|
|
||||||
token = encoder.Encode(payload, privateKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
private RSAParameters GetRSAPublic(string publicKey)
|
|
||||||
{
|
|
||||||
using (var tr = new System.IO.StringReader(publicKey))
|
|
||||||
{
|
|
||||||
var pemReader = new PemReader(tr);
|
|
||||||
var publicKeyParams = pemReader.ReadObject() as RsaKeyParameters;
|
|
||||||
if (publicKeyParams == null)
|
|
||||||
{
|
|
||||||
throw new Exception("Could not read RSA public key");
|
|
||||||
}
|
|
||||||
return DotNetUtilities.ToRSAParameters(publicKeyParams);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task<string> ReadKeyContent(string filepath)
|
|
||||||
{
|
|
||||||
return await System.IO.File.ReadAllTextAsync(filepath);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TokenRequest RetrieveTokenRequest()
|
|
||||||
{
|
|
||||||
_logger.Info("Retrieving token object");
|
|
||||||
|
|
||||||
return new TokenRequest
|
|
||||||
{
|
|
||||||
ClientId = _clientId, ClientSecret = _clientSecret,
|
|
||||||
Audience = _audience, GrantType = _grantType
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InitializeValues()
|
|
||||||
{
|
|
||||||
_logger.Info("Analyzing Auth0 information");
|
|
||||||
|
|
||||||
_clientId = _config["Auth0:ClientId"];
|
|
||||||
_clientSecret = _config["Auth0:ClientSecret"];
|
|
||||||
_audience = _config["Auth0:ApiIdentifier"];
|
|
||||||
_grantType = "client_credentials";
|
|
||||||
_url = $"https://{_config["Auth0:Domain"]}";
|
|
||||||
_privateKeyPath = _config["RSAKeys:PrivateKeyPath"];
|
|
||||||
_publicKeyPath = _config["RSAKeys:PublicKeyPath"];
|
|
||||||
_privateKey = System.IO.File.ReadAllText(_privateKeyPath);
|
|
||||||
_publicKey = System.IO.File.ReadAllText(_publicKeyPath);
|
|
||||||
|
|
||||||
PrintCredentials();
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Testing Methods
|
|
||||||
// For testing purposes
|
|
||||||
private void PrintCredentials()
|
|
||||||
{
|
|
||||||
Console.WriteLine("Auth0 credentials:");
|
|
||||||
Console.WriteLine($"Client Id: {_clientId}");
|
|
||||||
Console.WriteLine($"Client Secret: {_clientSecret}");
|
|
||||||
Console.WriteLine($"Audience: {_audience}");
|
|
||||||
Console.WriteLine($"Url: {_url}");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Classes
|
|
||||||
private class TokenRequest
|
|
||||||
{
|
|
||||||
[JsonProperty("client_id")]
|
|
||||||
public string ClientId { get; set; }
|
|
||||||
[JsonProperty("client_secret")]
|
|
||||||
public string ClientSecret { get; set; }
|
|
||||||
[JsonProperty("audience")]
|
|
||||||
public string Audience { get; set; }
|
|
||||||
[JsonProperty("grant_type")]
|
|
||||||
public string GrantType { get; set; }
|
|
||||||
}
|
|
||||||
private class TokenTierOne
|
|
||||||
{
|
|
||||||
[JsonProperty("access_token")]
|
|
||||||
public string AccessToken { get; set; }
|
|
||||||
[JsonProperty("expires_in")]
|
|
||||||
public int Expiration { get; set; }
|
|
||||||
[JsonProperty("token_type")]
|
|
||||||
public string TokenType { get; set; }
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NLog;
|
|
||||||
using TagLib;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Utilities
|
|
||||||
{
|
|
||||||
public class MetadataRetriever
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private static NLog.Logger _logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
|
|
||||||
private Song _updatedSong;
|
|
||||||
private string _message;
|
|
||||||
private string _title;
|
|
||||||
private string _artist;
|
|
||||||
private string _album;
|
|
||||||
private string _genre;
|
|
||||||
private int _year;
|
|
||||||
private int _duration;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
public Song UpdatedSongRecord
|
|
||||||
{
|
|
||||||
get => _updatedSong;
|
|
||||||
set => _updatedSong = value;
|
|
||||||
}
|
|
||||||
public string Message
|
|
||||||
{
|
|
||||||
get => _message;
|
|
||||||
set => _message = value;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public static void PrintMetadata(Song song)
|
|
||||||
{
|
|
||||||
Console.WriteLine("\n\nMetadata of the song:");
|
|
||||||
Console.WriteLine($"ID: {song.SongID}");
|
|
||||||
Console.WriteLine($"Title: {song.Title}");
|
|
||||||
Console.WriteLine($"Artist: {song.Artist}");
|
|
||||||
Console.WriteLine($"Album: {song.AlbumTitle}");
|
|
||||||
Console.WriteLine($"Genre: {song.Genre}");
|
|
||||||
Console.WriteLine($"Year: {song.Year}");
|
|
||||||
Console.WriteLine($"Duration: {song.Duration}");
|
|
||||||
Console.WriteLine($"AlbumID: {song.AlbumID}");
|
|
||||||
Console.WriteLine($"ArtistID: {song.ArtistID}");
|
|
||||||
Console.WriteLine($"GenreID: {song.GenreID}");
|
|
||||||
Console.WriteLine($"Song Path: {song.SongPath()}");
|
|
||||||
Console.WriteLine($"Filename: {song.Filename}");
|
|
||||||
Console.WriteLine("\n");
|
|
||||||
|
|
||||||
_logger.Info("Metadata of the song");
|
|
||||||
_logger.Info($"Title: {song.Title}");
|
|
||||||
_logger.Info($"Artist: {song.Artist}");
|
|
||||||
_logger.Info($"Album: {song.AlbumTitle}");
|
|
||||||
_logger.Info($"Genre: {song.Genre}");
|
|
||||||
_logger.Info($"Year: {song.Year}");
|
|
||||||
_logger.Info($"Duration: {song.Duration}");
|
|
||||||
}
|
|
||||||
public Song RetrieveMetaData(string filePath)
|
|
||||||
{
|
|
||||||
Song song = new Song();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
TagLib.File fileTag = TagLib.File.Create(filePath);
|
|
||||||
_title = fileTag.Tag.Title;
|
|
||||||
_artist = string.Join("", fileTag.Tag.Performers);
|
|
||||||
_album = fileTag.Tag.Album;
|
|
||||||
_genre = string.Join("", fileTag.Tag.Genres);
|
|
||||||
_year = (int)fileTag.Tag.Year;
|
|
||||||
_duration = (int)fileTag.Properties.Duration.TotalSeconds;
|
|
||||||
var albumArtist = string.Join("", fileTag.Tag.AlbumArtists);
|
|
||||||
var track = (int)fileTag.Tag.Track;
|
|
||||||
var disc = (int)fileTag.Tag.Disc;
|
|
||||||
|
|
||||||
song.Title = _title;
|
|
||||||
song.Artist = _artist;
|
|
||||||
song.AlbumTitle = _album;
|
|
||||||
song.AlbumArtist = albumArtist;
|
|
||||||
song.Genre = _genre;
|
|
||||||
song.Year = _year;
|
|
||||||
song.Duration = _duration;
|
|
||||||
song.Track = track;
|
|
||||||
song.Disc = disc;
|
|
||||||
song.TrackCount = (int)fileTag.Tag.TrackCount;
|
|
||||||
song.DiscCount = (int)fileTag.Tag.DiscCount;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
Console.WriteLine("An error occurred in MetadataRetriever");
|
|
||||||
Console.WriteLine(msg);
|
|
||||||
_logger.Error(msg, "An error occurred in MetadataRetriever");
|
|
||||||
}
|
|
||||||
|
|
||||||
return song;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int RetrieveSongDuration(string filepath)
|
|
||||||
{
|
|
||||||
var duration = 0;
|
|
||||||
var fileTag = TagLib.File.Create(filepath);
|
|
||||||
duration = (int)fileTag.Properties.Duration.TotalSeconds;
|
|
||||||
|
|
||||||
return duration;
|
|
||||||
}
|
|
||||||
|
|
||||||
public byte[] RetrieveCoverArtBytes(Song song)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine("Fetching image");
|
|
||||||
var tag = TagLib.File.Create(song.SongPath());
|
|
||||||
byte[] imgBytes = tag.Tag.Pictures[0].Data.Data;
|
|
||||||
|
|
||||||
return imgBytes;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred in MetadataRetriever");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public void UpdateMetadata(Song updatedSong, Song oldSong)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
InitializeUpdatedSong(oldSong);
|
|
||||||
var songValues = CheckSongValues(updatedSong);
|
|
||||||
PerformUpdate(updatedSong, songValues);
|
|
||||||
Message = "Successfully updated metadata";
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
Console.WriteLine($"An error occurred: {msg}");
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
Message = "Failed to update metadata";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public void UpdateCoverArt(Song song, CoverArt coverArt)
|
|
||||||
{
|
|
||||||
Console.WriteLine("Updating song's cover art");
|
|
||||||
|
|
||||||
var tag = TagLib.File.Create(song.SongPath());
|
|
||||||
var pics = tag.Tag.Pictures;
|
|
||||||
Array.Resize(ref pics, 1);
|
|
||||||
|
|
||||||
pics[0] = new Picture(coverArt.ImagePath)
|
|
||||||
{
|
|
||||||
Description = "Cover Art"
|
|
||||||
};
|
|
||||||
|
|
||||||
tag.Tag.Pictures = pics;
|
|
||||||
tag.Save();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void PerformUpdate(Song updatedSong, SortedDictionary<string, bool> checkedValues)
|
|
||||||
{
|
|
||||||
var filePath = updatedSong.SongPath();
|
|
||||||
var title = updatedSong.Title;
|
|
||||||
var artist = updatedSong.Artist;
|
|
||||||
var album = updatedSong.AlbumTitle;
|
|
||||||
var genre = updatedSong.Genre;
|
|
||||||
var year = updatedSong.Year;
|
|
||||||
var albumArtist = updatedSong.AlbumArtist;
|
|
||||||
var track = updatedSong.Track;
|
|
||||||
var trackCount = updatedSong.TrackCount;
|
|
||||||
var disc = updatedSong.Disc;
|
|
||||||
var discCount = updatedSong.DiscCount;
|
|
||||||
TagLib.File fileTag = TagLib.File.Create(filePath);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Updating metadata of {title}");
|
|
||||||
_logger.Info($"Updating metadata of {title}");
|
|
||||||
|
|
||||||
foreach (var key in checkedValues.Keys)
|
|
||||||
{
|
|
||||||
bool result = checkedValues[key];
|
|
||||||
|
|
||||||
if (!result)
|
|
||||||
switch (key.ToLower())
|
|
||||||
{
|
|
||||||
case "title":
|
|
||||||
_updatedSong.Title = title;
|
|
||||||
fileTag.Tag.Title = title;
|
|
||||||
break;
|
|
||||||
case "artists":
|
|
||||||
_updatedSong.Artist = artist;
|
|
||||||
fileTag.Tag.Performers = new []{artist};
|
|
||||||
break;
|
|
||||||
case "album":
|
|
||||||
_updatedSong.AlbumTitle = album;
|
|
||||||
fileTag.Tag.Album = album;
|
|
||||||
break;
|
|
||||||
case "genre":
|
|
||||||
_updatedSong.Genre = genre;
|
|
||||||
fileTag.Tag.Genres = new []{genre};
|
|
||||||
break;
|
|
||||||
case "year":
|
|
||||||
_updatedSong.Year = year;
|
|
||||||
fileTag.Tag.Year = (uint)year;
|
|
||||||
break;
|
|
||||||
case "albumartist":
|
|
||||||
_updatedSong.AlbumArtist = albumArtist;
|
|
||||||
fileTag.Tag.AlbumArtists = new []{albumArtist};
|
|
||||||
break;
|
|
||||||
case "track":
|
|
||||||
_updatedSong.Track = track;
|
|
||||||
fileTag.Tag.Track = (uint)(track);
|
|
||||||
break;
|
|
||||||
case "trackcount":
|
|
||||||
_updatedSong.TrackCount = trackCount;
|
|
||||||
fileTag.Tag.TrackCount = (uint)(trackCount);
|
|
||||||
break;
|
|
||||||
case "disc":
|
|
||||||
_updatedSong.Disc = disc;
|
|
||||||
fileTag.Tag.Disc = (uint)(disc);
|
|
||||||
break;
|
|
||||||
case "disccount":
|
|
||||||
_updatedSong.DiscCount = discCount;
|
|
||||||
fileTag.Tag.DiscCount = (uint)(discCount);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileTag.Save();
|
|
||||||
|
|
||||||
Console.WriteLine("Successfully updated metadata");
|
|
||||||
_logger.Info("Successfully updated metadata");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
Console.WriteLine($"An error occurred:\n{msg}");
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private void InitializeUpdatedSong(Song song)
|
|
||||||
{
|
|
||||||
_updatedSong = song;
|
|
||||||
}
|
|
||||||
private void PrintMetadata()
|
|
||||||
{
|
|
||||||
Console.WriteLine("\n\nMetadata of the song:");
|
|
||||||
Console.WriteLine($"Title: {_title}");
|
|
||||||
Console.WriteLine($"Artist: {_artist}");
|
|
||||||
Console.WriteLine($"Album: {_album}");
|
|
||||||
Console.WriteLine($"Genre: {_genre}");
|
|
||||||
Console.WriteLine($"Year: {_year}");
|
|
||||||
Console.WriteLine($"Duration: {_duration}\n\n");
|
|
||||||
|
|
||||||
_logger.Info("Metadata of the song");
|
|
||||||
_logger.Info($"Title: {_title}");
|
|
||||||
_logger.Info($"Artist: {_artist}");
|
|
||||||
_logger.Info($"Album: {_album}");
|
|
||||||
_logger.Info($"Genre: {_genre}");
|
|
||||||
_logger.Info($"Year: {_year}");
|
|
||||||
_logger.Info($"Duration: {_duration}");
|
|
||||||
}
|
|
||||||
private void PrintMetadata(Song song, string message)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"\n\n{message}");
|
|
||||||
Console.WriteLine($"Title: {song.Title}");
|
|
||||||
Console.WriteLine($"Artist: {song.Artist}");
|
|
||||||
Console.WriteLine($"Album: {song.AlbumTitle}");
|
|
||||||
Console.WriteLine($"Genre: {song.Genre}");
|
|
||||||
Console.WriteLine($"Year: {song.Year}");
|
|
||||||
Console.WriteLine($"Duration: {song.Duration}\n\n");
|
|
||||||
|
|
||||||
_logger.Info(message);
|
|
||||||
_logger.Info($"Title: {_title}");
|
|
||||||
_logger.Info($"Artist: {_artist}");
|
|
||||||
_logger.Info($"Album: {_album}");
|
|
||||||
_logger.Info($"Genre: {_genre}");
|
|
||||||
_logger.Info($"Year: {_year}");
|
|
||||||
_logger.Info($"Duration: {_duration}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private SortedDictionary<string, bool> CheckSongValues(Song song)
|
|
||||||
{
|
|
||||||
var songValues = new SortedDictionary<string, bool>();
|
|
||||||
Console.WriteLine("Checking for null data");
|
|
||||||
_logger.Info("Checking for null data");
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
songValues["Title"] = String.IsNullOrEmpty(song.Title);
|
|
||||||
songValues["Artists"] = String.IsNullOrEmpty(song.Artist);
|
|
||||||
songValues["Album"] = String.IsNullOrEmpty(song.AlbumTitle);
|
|
||||||
songValues["Genre"] = String.IsNullOrEmpty(song.Genre);
|
|
||||||
songValues["AlbumArtist"] = String.IsNullOrEmpty(song.AlbumArtist);
|
|
||||||
|
|
||||||
songValues["Year"] = CheckIntField(song.Year);
|
|
||||||
songValues["Track"] = CheckIntField(song.Track);
|
|
||||||
songValues["TrackCount"] = CheckIntField(song.TrackCount);
|
|
||||||
songValues["Disc"] = CheckIntField(song.Disc);
|
|
||||||
songValues["DiscCount"] = CheckIntField(song.Disc);
|
|
||||||
|
|
||||||
Console.WriteLine("Checking for null data completed");
|
|
||||||
_logger.Info("Checking for null data completed");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
Console.WriteLine($"An error occurred: \n{msg}");
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
return songValues;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool CheckIntField(int? value)
|
|
||||||
{
|
|
||||||
if (value == null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else if (value == 0)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Security.Cryptography;
|
|
||||||
|
|
||||||
using BCrypt.Net;
|
|
||||||
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
|
|
||||||
using NLog;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Utilities
|
|
||||||
{
|
|
||||||
public class PasswordEncryption
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private static Logger _logger = NLog.LogManager.GetCurrentClassLogger();
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public bool VerifyPassword(User user, string password)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = BCrypt.Net.BCrypt.Verify(password, user.Password);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.Error(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string HashPassword(User user)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string hashedPassword = string.Empty;
|
|
||||||
hashedPassword = BCrypt.Net.BCrypt.HashPassword(user.Password);
|
|
||||||
|
|
||||||
_logger.Info("Successfully hashed password");
|
|
||||||
|
|
||||||
return hashedPassword;
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exMsg = ex.Message;
|
|
||||||
_logger.Error(exMsg, "An error occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
string GenerateHash(string password, byte[] salt)
|
|
||||||
{
|
|
||||||
|
|
||||||
string hashed = Convert.ToBase64String(KeyDerivation.Pbkdf2(
|
|
||||||
password: password, salt: salt,
|
|
||||||
prf: KeyDerivationPrf.HMACSHA1,
|
|
||||||
iterationCount: 10000,
|
|
||||||
numBytesRequested: 256/8));
|
|
||||||
|
|
||||||
return hashed;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] GenerateSalt()
|
|
||||||
{
|
|
||||||
byte[] salt = new byte[128/8];
|
|
||||||
|
|
||||||
using (var rng = RandomNumberGenerator.Create())
|
|
||||||
rng.GetBytes(salt);
|
|
||||||
|
|
||||||
|
|
||||||
return salt;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
|
|
||||||
using Ionic.Zip;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.Utilities
|
|
||||||
{
|
|
||||||
public class SongCompression
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
string _compressedSongFilename;
|
|
||||||
string _tempDirectory;
|
|
||||||
byte[] _uncompressedSong;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Propterties
|
|
||||||
public string CompressedSongFilename
|
|
||||||
{
|
|
||||||
get => _compressedSongFilename;
|
|
||||||
set => _compressedSongFilename = value;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public SongCompression()
|
|
||||||
{
|
|
||||||
}
|
|
||||||
public SongCompression(string tempDirectory)
|
|
||||||
{
|
|
||||||
_tempDirectory = tempDirectory;
|
|
||||||
}
|
|
||||||
public SongCompression(byte[] uncompressedSong)
|
|
||||||
{
|
|
||||||
_uncompressedSong = uncompressedSong;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public async Task<SongData> RetrieveCompressedSong(Song song)
|
|
||||||
{
|
|
||||||
SongData songData = new SongData();
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var archivePath = RetrieveCompressesSongPath(song);
|
|
||||||
Console.WriteLine($"Compressed song saved to: {archivePath}");
|
|
||||||
|
|
||||||
songData.Data = await System.IO.File.ReadAllBytesAsync(archivePath);
|
|
||||||
}
|
|
||||||
catch(Exception ex)
|
|
||||||
{
|
|
||||||
var exMsg = ex.Message;
|
|
||||||
Console.WriteLine($"An error ocurred: \n{exMsg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
return songData;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string RetrieveCompressesSongPath(Song songDetails)
|
|
||||||
{
|
|
||||||
string tmpZipFilePath = _tempDirectory + songDetails.Filename;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
using (ZipFile zip = new ZipFile())
|
|
||||||
{
|
|
||||||
zip.AddFile(songDetails.SongPath());
|
|
||||||
zip.Save(tmpZipFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.WriteLine("Successfully compressed");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exMsg = ex.Message;
|
|
||||||
Console.WriteLine("An error ocurred");
|
|
||||||
Console.WriteLine(exMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (songDetails.Filename.Contains(".mp3"))
|
|
||||||
{
|
|
||||||
_compressedSongFilename = StripMP3Extension(songDetails.Filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tmpZipFilePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Method not being used
|
|
||||||
public byte[] CompressedSong(byte[] uncompressedSong)
|
|
||||||
{
|
|
||||||
byte[] compressedSong = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Console.WriteLine("Song has been successfully compressed");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var exMsg = ex.Message;
|
|
||||||
Console.WriteLine("An error ocurred:");
|
|
||||||
Console.WriteLine(exMsg);
|
|
||||||
}
|
|
||||||
|
|
||||||
return compressedSong;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
string StripMP3Extension(string filename)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Before: {filename}");
|
|
||||||
int filenameLength = filename.Length;
|
|
||||||
Console.WriteLine($"Filename length {filenameLength}");
|
|
||||||
var endIndex = filenameLength - 1;
|
|
||||||
var startIndex = endIndex - 3;
|
|
||||||
Console.WriteLine($"Starting index {startIndex} and ending index {endIndex}");
|
|
||||||
var stripped = filename.Remove(startIndex, 4);
|
|
||||||
stripped += ".zip";
|
|
||||||
Console.WriteLine($"After {stripped}");
|
|
||||||
|
|
||||||
return stripped;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/album")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class AlbumController : BaseController
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private readonly ILogger<AlbumController> _logger;
|
|
||||||
private string _connectionString;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public AlbumController(ILogger<AlbumController> logger, IConfiguration config)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region HTTP Routes
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult GetAlbums()
|
|
||||||
{
|
|
||||||
List<Album> albums = new List<Album>();
|
|
||||||
|
|
||||||
var albumContext = new AlbumContext(_connectionString);
|
|
||||||
|
|
||||||
albums = albumContext.Albums.ToList();
|
|
||||||
|
|
||||||
if (albums.Count > 0)
|
|
||||||
return Ok(albums);
|
|
||||||
else
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public IActionResult GetAlbum(int id)
|
|
||||||
{
|
|
||||||
Album album = new Album
|
|
||||||
{
|
|
||||||
AlbumID = id
|
|
||||||
};
|
|
||||||
|
|
||||||
var albumContext = new AlbumContext(_connectionString);
|
|
||||||
|
|
||||||
if (albumContext.DoesRecordExist(album))
|
|
||||||
{
|
|
||||||
album = albumContext.RetrieveRecord(album);
|
|
||||||
|
|
||||||
return Ok(album);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/artist")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class ArtistController : BaseController
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private readonly ILogger<ArtistController> _logger;
|
|
||||||
private string _connectionString;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public ArtistController(ILogger<ArtistController> logger, IConfiguration config)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region HTTP Routes
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult GetArtists()
|
|
||||||
{
|
|
||||||
var artistContext = new ArtistContext(_connectionString);
|
|
||||||
|
|
||||||
var artists = artistContext.Artists.ToList();
|
|
||||||
|
|
||||||
if (artists.Count > 0)
|
|
||||||
return Ok(artists);
|
|
||||||
else
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public IActionResult GetArtist(int id)
|
|
||||||
{
|
|
||||||
if (!IsTokenValid("read:artists"))
|
|
||||||
{
|
|
||||||
return StatusCode(401, "Not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
Artist artist = new Artist
|
|
||||||
{
|
|
||||||
ArtistID = id
|
|
||||||
};
|
|
||||||
|
|
||||||
var artistContext = new ArtistContext(_connectionString);
|
|
||||||
|
|
||||||
if (artistContext.DoesRecordExist(artist))
|
|
||||||
{
|
|
||||||
artist = artistContext.RetrieveRecord(artist);
|
|
||||||
|
|
||||||
return Ok(artist);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Managers;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
public class BaseController : ControllerBase
|
|
||||||
{
|
|
||||||
#region Fiends
|
|
||||||
protected IConfiguration _config;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
protected string ParseBearerTokenFromHeader()
|
|
||||||
{
|
|
||||||
var token = string.Empty;
|
|
||||||
const string tokenType = "Bearer";
|
|
||||||
const string otherTokenType = "Jwt";
|
|
||||||
|
|
||||||
var req = Request;
|
|
||||||
var auth = req.Headers.Authorization;
|
|
||||||
var val = auth.ToString();
|
|
||||||
|
|
||||||
if ((val.Contains(tokenType) || val.Contains(otherTokenType)) && val.Split(" ").Count() > 1)
|
|
||||||
{
|
|
||||||
var split = val.Split(" ");
|
|
||||||
token = split[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
[ApiExplorerSettings(IgnoreApi = true)]
|
|
||||||
protected bool IsTokenValid(string scope)
|
|
||||||
{
|
|
||||||
var token = ParseBearerTokenFromHeader();
|
|
||||||
var tokMgr = new TokenManager(_config);
|
|
||||||
|
|
||||||
return tokMgr.IsTokenValid(scope, token);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Managers;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/coverart")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class CoverArtController : BaseController
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private readonly ILogger<CoverArtController> _logger;
|
|
||||||
private string _connectionString;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public CoverArtController(ILogger<CoverArtController> logger, IConfiguration config)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region HTTP Routes
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult GetCoverArts()
|
|
||||||
{
|
|
||||||
var coverArtContext = new CoverArtContext(_connectionString);
|
|
||||||
|
|
||||||
var coverArtRecords = coverArtContext.CoverArtImages.ToList();
|
|
||||||
|
|
||||||
if (coverArtRecords == null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("No cover art records");
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Found cover art records");
|
|
||||||
return Ok(coverArtRecords);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public IActionResult GetCoverArt(int id)
|
|
||||||
{
|
|
||||||
var coverArt = new CoverArt { CoverArtID = id };
|
|
||||||
|
|
||||||
var coverArtContext = new CoverArtContext(_connectionString);
|
|
||||||
|
|
||||||
coverArt = coverArtContext.RetrieveRecord(coverArt);
|
|
||||||
|
|
||||||
if (coverArt != null)
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Found cover art record");
|
|
||||||
var coverArtBytes = System.IO.File.ReadAllBytes(
|
|
||||||
coverArt.ImagePath);
|
|
||||||
|
|
||||||
return File(coverArtBytes, "application/x-msdownload",
|
|
||||||
coverArt.SongTitle);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Cover art not found");
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/genre")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class GenreController : BaseController
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private readonly ILogger<GenreController> _logger;
|
|
||||||
private string _connectionString;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public GenreController(ILogger<GenreController> logger, IConfiguration config)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region HTTP Routes
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult GetGenres()
|
|
||||||
{
|
|
||||||
var genres = new List<Genre>();
|
|
||||||
|
|
||||||
var genreStore = new GenreContext(_connectionString);
|
|
||||||
|
|
||||||
genres = genreStore.Genres.ToList();
|
|
||||||
|
|
||||||
if (genres.Count > 0)
|
|
||||||
return Ok(genres);
|
|
||||||
else
|
|
||||||
return NotFound(new List<Genre>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public IActionResult GetGenre(int id)
|
|
||||||
{
|
|
||||||
var genre = new Genre
|
|
||||||
{
|
|
||||||
GenreID = id
|
|
||||||
};
|
|
||||||
|
|
||||||
var genreStore = new GenreContext(_connectionString);
|
|
||||||
|
|
||||||
if (genreStore.DoesRecordExist(genre))
|
|
||||||
{
|
|
||||||
genre = genreStore.RetrieveRecord(genre);
|
|
||||||
|
|
||||||
return Ok(genre);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
return NotFound(new Genre());
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Managers;
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/login")]
|
|
||||||
[ApiController]
|
|
||||||
public class LoginController : ControllerBase
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private string _connectionString;
|
|
||||||
private IConfiguration _config;
|
|
||||||
private ILogger<LoginController> _logger;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Contructors
|
|
||||||
public LoginController(IConfiguration config, ILogger<LoginController> logger)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region HTTP endpoints
|
|
||||||
[HttpPost]
|
|
||||||
public IActionResult Login([FromBody] User user)
|
|
||||||
{
|
|
||||||
var context = new UserContext(_connectionString);
|
|
||||||
|
|
||||||
_logger.LogInformation("Starting process of validating credentials");
|
|
||||||
|
|
||||||
var message = "Invalid credentials";
|
|
||||||
var password = user.Password;
|
|
||||||
|
|
||||||
var loginRes = new LoginResult
|
|
||||||
{
|
|
||||||
Username = user.Username
|
|
||||||
};
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username)) != null)
|
|
||||||
{
|
|
||||||
user = context.Users.FirstOrDefault(usr => usr.Username.Equals(user.Username));
|
|
||||||
|
|
||||||
var validatePass = new PasswordEncryption();
|
|
||||||
var validated = validatePass.VerifyPassword(user, password);
|
|
||||||
if (!validated)
|
|
||||||
{
|
|
||||||
loginRes.Message = message;
|
|
||||||
_logger.LogInformation(message);
|
|
||||||
|
|
||||||
return Ok(loginRes);
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogInformation("Successfully validated user credentials");
|
|
||||||
|
|
||||||
TokenManager tk = new TokenManager(_config);
|
|
||||||
|
|
||||||
loginRes = tk.LoginSymmetric(user);
|
|
||||||
|
|
||||||
return Ok(loginRes);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
loginRes.Message = message;
|
|
||||||
|
|
||||||
return NotFound(loginRes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError("An error occurred: {0}", ex.Message);
|
|
||||||
_logger.LogError("Inner Exception: {0}", ex.InnerException.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NotFound(loginRes);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Managers;
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/register")]
|
|
||||||
[ApiController]
|
|
||||||
public class RegisterController : ControllerBase
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private string _connectionString;
|
|
||||||
private IConfiguration _config;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
public RegisterController(IConfiguration config)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public IActionResult RegisterUser([FromBody] User user)
|
|
||||||
{
|
|
||||||
PasswordEncryption pe = new PasswordEncryption();
|
|
||||||
user.Password = pe.HashPassword(user);
|
|
||||||
user.EmailVerified = false;
|
|
||||||
user.Status = "Registered";
|
|
||||||
user.DateCreated = DateTime.Now;
|
|
||||||
|
|
||||||
UserContext context = null;
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
context = new UserContext(_config.GetConnectionString("DefaultConnection"));
|
|
||||||
context.Add(user);
|
|
||||||
context.SaveChanges();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
var stackTrace = ex.StackTrace;
|
|
||||||
|
|
||||||
Console.WriteLine($"An error occurred: {msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var registerResult = new RegisterResult
|
|
||||||
{
|
|
||||||
Username = user.Username
|
|
||||||
};
|
|
||||||
|
|
||||||
if (context.Users.FirstOrDefault(sng => sng.Username.Equals(user.Username)) != null)
|
|
||||||
{
|
|
||||||
registerResult.Message = "Successful registration";
|
|
||||||
registerResult.SuccessfullyRegistered = true;
|
|
||||||
|
|
||||||
return Ok(registerResult);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
registerResult.Message = "Registration failed";
|
|
||||||
registerResult.SuccessfullyRegistered = false;
|
|
||||||
|
|
||||||
return Ok(registerResult);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Managers;
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/song/compressed/data")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class SongCompressedDataController : BaseController
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private string _connectionString;
|
|
||||||
private string _songTempDir;
|
|
||||||
private string _archiveDir;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
public SongCompressedDataController(IConfiguration config)
|
|
||||||
{
|
|
||||||
_songTempDir = _config.GetValue<string>("TemporaryMusicPath");
|
|
||||||
_archiveDir = _config.GetValue<string>("ArchivePath");
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region API Routes
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public async Task<IActionResult> DownloadCompressedSong(int id)
|
|
||||||
{
|
|
||||||
var context = new SongContext(_connectionString);
|
|
||||||
|
|
||||||
SongCompression cmp = new SongCompression(_archiveDir);
|
|
||||||
|
|
||||||
Console.WriteLine($"Archive directory root: {_archiveDir}");
|
|
||||||
|
|
||||||
Console.WriteLine("Starting process of retrieving comrpessed song");
|
|
||||||
SongData song = await cmp.RetrieveCompressedSong(context.RetrieveRecord(new Song{ SongID = id }));
|
|
||||||
|
|
||||||
return File(song.Data, "application/x-msdownload", cmp.CompressedSongFilename);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Managers;
|
|
||||||
using Icarus.Controllers.Utilities;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/song")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class SongController : BaseController
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private readonly ILogger<SongController> _logger;
|
|
||||||
private string _connectionString;
|
|
||||||
private SongManager _songMgr;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
public SongController(IConfiguration config, ILogger<SongController> logger)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
_logger = logger;
|
|
||||||
_songMgr = new SongManager(config);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
#region HTTP Endpoints
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
public IActionResult GetSongs()
|
|
||||||
{
|
|
||||||
List<Song> songs = new List<Song>();
|
|
||||||
Console.WriteLine("Attemtping to retrieve songs");
|
|
||||||
_logger.LogInformation("Attempting to retrieve songs");
|
|
||||||
|
|
||||||
var context = new SongContext(_connectionString);
|
|
||||||
|
|
||||||
songs = context.Songs.ToList();
|
|
||||||
|
|
||||||
if (songs.Count > 0)
|
|
||||||
return Ok(songs);
|
|
||||||
else
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public IActionResult GetSong(int id)
|
|
||||||
{
|
|
||||||
var context = new SongContext(_connectionString);
|
|
||||||
|
|
||||||
Song song = new Song { SongID = id };
|
|
||||||
song = context.RetrieveRecord(song);
|
|
||||||
|
|
||||||
Console.WriteLine("Here");
|
|
||||||
|
|
||||||
if (song.SongID != 0)
|
|
||||||
return Ok(song);
|
|
||||||
else
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPut("{id}")]
|
|
||||||
public IActionResult UpdateSong(int id, [FromBody] Song song)
|
|
||||||
{
|
|
||||||
var context = new SongContext(_connectionString);
|
|
||||||
|
|
||||||
song.SongID = id;
|
|
||||||
Console.WriteLine("Retrieving filepath of song");
|
|
||||||
_logger.LogInformation("Retrieving filepath of song");
|
|
||||||
|
|
||||||
if (!_songMgr.DoesSongExist(song))
|
|
||||||
{
|
|
||||||
return NotFound(new SongResult
|
|
||||||
{
|
|
||||||
Message = "Song does not exist"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
var songRes = _songMgr.UpdateSong(song);
|
|
||||||
|
|
||||||
return Ok(songRes);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,167 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Configuration;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
using Icarus.Controllers.Managers;
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/song/data")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class SongDataController : BaseController
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private string _connectionString;
|
|
||||||
private ILogger<SongDataController> _logger;
|
|
||||||
private SongManager _songMgr;
|
|
||||||
private string _songTempDir;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
public SongDataController(IConfiguration config, ILogger<SongDataController> logger)
|
|
||||||
{
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
_logger = logger;
|
|
||||||
_songTempDir = _config.GetValue<string>("TemporaryMusicPath");
|
|
||||||
_songMgr = new SongManager(config, _songTempDir);
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
[HttpGet("download/{id}")]
|
|
||||||
public IActionResult Download(int id)
|
|
||||||
{
|
|
||||||
var songContext = new SongContext(_connectionString);
|
|
||||||
var songMetaData = songContext.RetrieveRecord(new Song { SongID = id});
|
|
||||||
|
|
||||||
var song = _songMgr.RetrieveSong(songMetaData).Result;
|
|
||||||
|
|
||||||
return File(song.Data, "application/x-msdownload", songMetaData.Filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assumes that the song already has metadata such as
|
|
||||||
// Title
|
|
||||||
// Artist
|
|
||||||
// Album
|
|
||||||
// Genre
|
|
||||||
// Year
|
|
||||||
// Track
|
|
||||||
// Track count
|
|
||||||
// Disc
|
|
||||||
// Disc count
|
|
||||||
// Cover art
|
|
||||||
//
|
|
||||||
[HttpPost("upload"), DisableRequestSizeLimit]
|
|
||||||
public IActionResult Upload([FromForm(Name = "file")] List<IFormFile> songData)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Console.WriteLine("Uploading song...");
|
|
||||||
_logger.LogInformation("Uploading song...");
|
|
||||||
|
|
||||||
var uploads = _songTempDir;
|
|
||||||
// Console.WriteLine($"Song Root Path {uploads}");
|
|
||||||
_logger.LogInformation($"Song root path {uploads}");
|
|
||||||
|
|
||||||
foreach (var sng in songData)
|
|
||||||
if (sng.Length > 0)
|
|
||||||
{
|
|
||||||
// Console.WriteLine($"Song filename {sng.FileName}");
|
|
||||||
_logger.LogInformation($"Song filename {sng.FileName}");
|
|
||||||
|
|
||||||
_songMgr.SaveSongToFileSystem(sng).Wait();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
var msg = ex.Message;
|
|
||||||
_logger.LogError(msg, "An error occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
return NotFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
// The client is expected to send the file, metadata, and cover art separately.
|
|
||||||
// Any metadata already on the file will be overwritten with values from the metadata
|
|
||||||
// as well as the cover art
|
|
||||||
//
|
|
||||||
[HttpPost("upload/with/data")]
|
|
||||||
public IActionResult UploadWithData([FromForm] UploadSongWithDataForm up)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (up.SongData.Length > 0 && up.CoverArtData.Length > 0 && !string.IsNullOrEmpty(up.SongFile))
|
|
||||||
{
|
|
||||||
var song = Newtonsoft.Json.JsonConvert.DeserializeObject<Song>(up.SongFile);
|
|
||||||
_logger.LogInformation($"Song title: {song.Title}");
|
|
||||||
|
|
||||||
_songMgr.SaveSongToFileSystem(up.SongData, up.CoverArtData, song);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex.Message, "An error occurred");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpDelete("delete/{id}")]
|
|
||||||
public IActionResult DeleteSong(int id)
|
|
||||||
{
|
|
||||||
var songContext = new SongContext(_connectionString);
|
|
||||||
|
|
||||||
var songMetaData = new Song{ SongID = id };
|
|
||||||
Console.WriteLine($"Id {songMetaData.SongID}");
|
|
||||||
|
|
||||||
songMetaData = songContext.RetrieveRecord(songMetaData);
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(songMetaData.Title))
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Song does not exist");
|
|
||||||
return NotFound("Song does not exist");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_logger.LogInformation("Starting process of deleting song from the filesystem and database");
|
|
||||||
|
|
||||||
_songMgr.DeleteSong(songMetaData);
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UploadSongWithDataForm
|
|
||||||
{
|
|
||||||
[FromForm(Name = "file")]
|
|
||||||
public IFormFile SongData { get; set; }
|
|
||||||
// NOTE: Think about making this optional and if it is not provided, use the stock cover art
|
|
||||||
[FromForm(Name = "cover")]
|
|
||||||
public IFormFile CoverArtData { get; set; }
|
|
||||||
[FromForm(Name = "metadata")]
|
|
||||||
public string SongFile { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http.Headers;
|
|
||||||
using System.Web;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
using Icarus.Controllers.Managers;
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus.Controllers.V1
|
|
||||||
{
|
|
||||||
[Route("api/v1/song/stream")]
|
|
||||||
[ApiController]
|
|
||||||
[Authorize]
|
|
||||||
public class SongStreamController : BaseController
|
|
||||||
{
|
|
||||||
#region Fields
|
|
||||||
private ILogger<SongStreamController> _logger;
|
|
||||||
private string _connectionString;
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructor
|
|
||||||
public SongStreamController(ILogger<SongStreamController> logger, IConfiguration config)
|
|
||||||
{
|
|
||||||
_logger = logger;
|
|
||||||
_config = config;
|
|
||||||
_connectionString = _config.GetConnectionString("DefaultConnection");
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region HTTP endpoints
|
|
||||||
[HttpGet("{id}")]
|
|
||||||
public async Task<IActionResult> StreamSong(int id)
|
|
||||||
{
|
|
||||||
var context = new SongContext(_config.GetConnectionString("DefaultConnection"));
|
|
||||||
|
|
||||||
var song = context.Songs.FirstOrDefault(sng => sng.SongID == id);
|
|
||||||
|
|
||||||
var stream = new FileStream(song.SongPath(), FileMode.Open, FileAccess.Read);
|
|
||||||
stream.Position = 0;
|
|
||||||
var filename = $"{song.Title}.mp3";
|
|
||||||
|
|
||||||
_logger.LogInformation("Starting to stream song...>");
|
|
||||||
Console.WriteLine("Starting to streamsong...");
|
|
||||||
|
|
||||||
var file = await Task.Run(() => {
|
|
||||||
return File(stream, "application/octet-stream", filename);
|
|
||||||
});
|
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Database.Contexts
|
|
||||||
{
|
|
||||||
public class AlbumContext : DbContext
|
|
||||||
{
|
|
||||||
public DbSet<Album> Albums { get; set; }
|
|
||||||
|
|
||||||
public AlbumContext(DbContextOptions<AlbumContext> options) : base(options) { }
|
|
||||||
public AlbumContext(string connString) : base(new DbContextOptionsBuilder<AlbumContext>()
|
|
||||||
.UseMySQL(connString).Options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Album>()
|
|
||||||
.ToTable("Album");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Album RetrieveRecord(Album album)
|
|
||||||
{
|
|
||||||
return Albums.FirstOrDefault(alb => alb.AlbumID == album.AlbumID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DoesRecordExist(Album album)
|
|
||||||
{
|
|
||||||
return Albums.FirstOrDefault(alb => alb.AlbumID == album.AlbumID) != null ? true : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Database.Contexts
|
|
||||||
{
|
|
||||||
public class ArtistContext : DbContext
|
|
||||||
{
|
|
||||||
public DbSet<Artist> Artists { get; set; }
|
|
||||||
|
|
||||||
public ArtistContext(DbContextOptions<ArtistContext> options) : base (options) { }
|
|
||||||
public ArtistContext(string connString) : base(new DbContextOptionsBuilder<ArtistContext>()
|
|
||||||
.UseMySQL(connString).Options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Artist>()
|
|
||||||
.ToTable("Artist");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Artist RetrieveRecord(Artist artist)
|
|
||||||
{
|
|
||||||
|
|
||||||
return Artists.FirstOrDefault(arst => arst.ArtistID == artist.ArtistID);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public bool DoesRecordExist(Artist artist)
|
|
||||||
{
|
|
||||||
return Artists.FirstOrDefault(arst => arst.ArtistID == artist.ArtistID) != null ? true : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Database.Contexts
|
|
||||||
{
|
|
||||||
public class CoverArtContext : DbContext
|
|
||||||
{
|
|
||||||
public DbSet<CoverArt> CoverArtImages { get; set; }
|
|
||||||
|
|
||||||
public CoverArtContext(DbContextOptions<CoverArtContext> options) : base(options) { }
|
|
||||||
public CoverArtContext(string connString) : base(new DbContextOptionsBuilder<CoverArtContext>()
|
|
||||||
.UseMySQL(connString).Options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<CoverArt>()
|
|
||||||
.ToTable("CoverArt");
|
|
||||||
}
|
|
||||||
|
|
||||||
public CoverArt RetrieveRecord(CoverArt cover)
|
|
||||||
{
|
|
||||||
return CoverArtImages.FirstOrDefault(cov => cov.CoverArtID == cover.CoverArtID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DoesRecordExist(CoverArt cover)
|
|
||||||
{
|
|
||||||
return CoverArtImages.FirstOrDefault(cov => cov.CoverArtID == cover.CoverArtID) != null ? true : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Database.Contexts
|
|
||||||
{
|
|
||||||
public class GenreContext : DbContext
|
|
||||||
{
|
|
||||||
public DbSet<Genre> Genres { get; set; }
|
|
||||||
public GenreContext(DbContextOptions<GenreContext> options) : base(options) { }
|
|
||||||
public GenreContext(string connString) : base(new DbContextOptionsBuilder<GenreContext>()
|
|
||||||
.UseMySQL(connString).Options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Genre>()
|
|
||||||
.ToTable("Genre");
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Genre RetrieveRecord(Genre genre)
|
|
||||||
{
|
|
||||||
return Genres.FirstOrDefault(gnr => gnr.GenreID == genre.GenreID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DoesRecordExist(Genre genre)
|
|
||||||
{
|
|
||||||
return Genres.FirstOrDefault(gnr => gnr.GenreID == genre.GenreID) != null ? true : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Database.Contexts
|
|
||||||
{
|
|
||||||
public class SongContext : DbContext
|
|
||||||
{
|
|
||||||
public DbSet<Song> Songs { get; set; }
|
|
||||||
|
|
||||||
public SongContext(string connString) : base(new DbContextOptionsBuilder<SongContext>()
|
|
||||||
.UseMySQL(connString).Options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public SongContext(DbContextOptions<SongContext> options) : base(options) { }
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.ToTable("Song");
|
|
||||||
|
|
||||||
/**
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.HasOne(s => s.Album)
|
|
||||||
.WithMany(al => al.Songs)
|
|
||||||
.HasForeignKey(s => s.AlbumID)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.HasOne(sa => sa.SongArtist)
|
|
||||||
.WithMany(ar => ar.Songs)
|
|
||||||
.HasForeignKey(s => s.ArtistID)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.HasOne(s => s.SongGenre)
|
|
||||||
.WithMany(gnr => gnr.Songs)
|
|
||||||
.HasForeignKey(s => s.GenreID)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.HasOne(s => s.SongYear)
|
|
||||||
.WithMany(yr => yr.Songs)
|
|
||||||
.HasForeignKey(s => s.YearID)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.HasOne(s => s.SongCoverArt)
|
|
||||||
.WithMany(ca => ca.Songs)
|
|
||||||
.HasForeignKey(s => s.CoverArtID)
|
|
||||||
.OnDelete(DeleteBehavior.SetNull);
|
|
||||||
*/
|
|
||||||
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.Property(s => s.Year)
|
|
||||||
.IsRequired(false);
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.Property(s => s.GenreID)
|
|
||||||
.IsRequired(false);
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.Property(s => s.ArtistID)
|
|
||||||
.IsRequired(false);
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.Property(s => s.AlbumID)
|
|
||||||
.IsRequired(false);
|
|
||||||
modelBuilder.Entity<Song>()
|
|
||||||
.Property(s => s.CoverArtID)
|
|
||||||
.IsRequired(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public Song RetrieveRecord(Song song)
|
|
||||||
{
|
|
||||||
return Songs.FirstOrDefault(sng => sng.SongID == song.SongID);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public bool DoesRecordExist(Song song)
|
|
||||||
{
|
|
||||||
return Songs.FirstOrDefault(sng => sng.SongID == song.SongID) != null ? true : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
// using MySql.Data.EntityFrameworkCore;
|
|
||||||
// using MySql.Data;
|
|
||||||
// using MySql.Data.EntityFrameworkCore.Extensions;
|
|
||||||
// using MySql.Data.Entity;
|
|
||||||
// using MySql.Data.MySqlClient;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
using Icarus.Models;
|
|
||||||
|
|
||||||
namespace Icarus.Database.Contexts
|
|
||||||
{
|
|
||||||
public class UserContext : DbContext
|
|
||||||
{
|
|
||||||
public DbSet<User> Users { get; set; }
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
public UserContext(DbContextOptions<UserContext> options) : base(options) { }
|
|
||||||
[ActivatorUtilitiesConstructor]
|
|
||||||
public UserContext(string connString) : base(new DbContextOptionsBuilder<UserContext>()
|
|
||||||
.UseMySQL(connString).Options)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
|
||||||
{
|
|
||||||
modelBuilder.Entity<User>()
|
|
||||||
.ToTable("User");
|
|
||||||
modelBuilder.Entity<User>()
|
|
||||||
.Property(u => u.LastLogin).IsRequired(false);
|
|
||||||
modelBuilder.Entity<User>()
|
|
||||||
.Property(u => u.DateCreated).HasDefaultValue(DateTime.Now);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public User RetrieveRecord(User user)
|
|
||||||
{
|
|
||||||
return Users.FirstOrDefault(usr => usr.UserID == user.UserID);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool DoesRecordExist(User user)
|
|
||||||
{
|
|
||||||
return Users.FirstOrDefault(usr => usr.UserID == user.UserID) != null ? true : false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Stage 1: Build the application
|
||||||
|
FROM rust:1.88 as builder
|
||||||
|
|
||||||
|
# Set the working directory inside the container
|
||||||
|
WORKDIR /usr/src/app
|
||||||
|
|
||||||
|
# Install build dependencies if needed (e.g., git for cloning)
|
||||||
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
|
pkg-config libssl3 \
|
||||||
|
ca-certificates \
|
||||||
|
openssh-client git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Create .ssh/ directory for internal dependencies
|
||||||
|
RUN mkdir -p -m 0700 ~/.ssh && \
|
||||||
|
echo "Host git.kundeng.us" >> ~/.ssh/config && \
|
||||||
|
echo " User git" >> ~/.ssh/config && \
|
||||||
|
chmod 600 ~/.ssh/config
|
||||||
|
|
||||||
|
# << --- ADD HOST KEY HERE --- >>
|
||||||
|
RUN ssh-keyscan git.kundeng.us >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
|
# Copy Cargo manifests
|
||||||
|
COPY Cargo.toml Cargo.lock ./
|
||||||
|
|
||||||
|
# Build *only* dependencies to leverage Docker cache
|
||||||
|
# This dummy build caches dependencies as a separate layer
|
||||||
|
RUN --mount=type=ssh mkdir src && \
|
||||||
|
echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs && \
|
||||||
|
cargo build --release --quiet && \
|
||||||
|
rm -rf src target/release/deps/icarus* # Clean up dummy build artifacts (replace icarus)
|
||||||
|
|
||||||
|
# Copy the actual source code
|
||||||
|
COPY src ./src
|
||||||
|
# If you have other directories like `templates` or `static`, copy them too
|
||||||
|
COPY .env ./.env
|
||||||
|
COPY migrations ./migrations
|
||||||
|
|
||||||
|
# << --- SSH MOUNT ADDED HERE --- >>
|
||||||
|
# Build *only* dependencies to leverage Docker cache
|
||||||
|
# This dummy build caches dependencies as a separate layer
|
||||||
|
# Mount the SSH agent socket for this command
|
||||||
|
RUN --mount=type=ssh \
|
||||||
|
cargo build --release --quiet
|
||||||
|
|
||||||
|
# Stage 2: Create the final, smaller runtime image
|
||||||
|
# Use a minimal base image like debian-slim or even distroless for security/size
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
|
||||||
|
# Install runtime dependencies if needed (e.g., SSL certificates)
|
||||||
|
RUN apt-get update && apt-get install -y ca-certificates libssl-dev libssl3 && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /usr/local/bin
|
||||||
|
|
||||||
|
# Copy the compiled binary from the builder stage
|
||||||
|
# Replace 'icarus' with the actual name of your binary (usually the crate name)
|
||||||
|
COPY --from=builder /usr/src/app/target/release/icarus .
|
||||||
|
|
||||||
|
# Copy other necessary files like .env (if used for runtime config) or static assets
|
||||||
|
# It's generally better to configure via environment variables in Docker though
|
||||||
|
COPY --from=builder /usr/src/app/.env .
|
||||||
|
COPY --from=builder /usr/src/app/migrations ./migrations
|
||||||
|
|
||||||
|
# Expose the port your Axum app listens on (e.g., 3000 or 8000)
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Set the command to run your application
|
||||||
|
# Ensure this matches the binary name copied above
|
||||||
|
CMD ["./icarus"]
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net6</TargetFramework>
|
|
||||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
|
||||||
<Version>0.1.10</Version>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="BCrypt.Net-Next" Version="4.0.2" />
|
|
||||||
<PackageReference Include="DotNetZip" Version="1.15.0" />
|
|
||||||
<PackageReference Include="EntityFramework" Version="6.4.4" />
|
|
||||||
<PackageReference Include="ID3" Version="0.6.0" />
|
|
||||||
<PackageReference Include="JWT" Version="9.0.3" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="5.0.8" />
|
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="5.0.8">
|
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
|
||||||
<PrivateAssets>all</PrivateAssets>
|
|
||||||
</PackageReference>
|
|
||||||
<PackageReference Include="MySql.EntityFrameworkCore" Version="5.0.8" />
|
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
|
||||||
<PackageReference Include="NLog.Web.AspNetCore" Version="4.14.0" />
|
|
||||||
<PackageReference Include="Portable.BouncyCastle" Version="1.9.0" />
|
|
||||||
<PackageReference Include="RestSharp" Version="106.15.0" />
|
|
||||||
<PackageReference Include="SevenZip" Version="19.0.0" />
|
|
||||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
|
||||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.15.0" />
|
|
||||||
<PackageReference Include="taglib" Version="2.1.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
<ItemGroup>
|
|
||||||
<Content Update="nlog.config" CopyToOutputDirectory="PreserveNewest" />
|
|
||||||
<Content Include="Images/Stock/*.*" CopyToOutputDirectory="PreserveNewest" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
|
||||||
# Visual Studio Version 17
|
|
||||||
VisualStudioVersion = 17.2.32616.157
|
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Icarus", "Icarus.csproj", "{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}"
|
|
||||||
EndProject
|
|
||||||
Global
|
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
|
||||||
Debug|Any CPU = Debug|Any CPU
|
|
||||||
Release|Any CPU = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
|
||||||
{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{66389DAC-0D3C-4271-BBAE-AF27FCFD28B7}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
|
||||||
HideSolutionNode = FALSE
|
|
||||||
EndGlobalSection
|
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
|
||||||
SolutionGuid = {0BE61157-7A0C-41CF-B097-71178A3F6755}
|
|
||||||
EndGlobalSection
|
|
||||||
EndGlobal
|
|
||||||
|
Before Width: | Height: | Size: 107 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 24 KiB |
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2021 Kun Deng
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class Album
|
|
||||||
{
|
|
||||||
[JsonProperty("album_id")]
|
|
||||||
public int AlbumID { get; set; }
|
|
||||||
[JsonProperty("title")]
|
|
||||||
public string Title { get; set; }
|
|
||||||
[JsonProperty("album_artist")]
|
|
||||||
[Column("Artist")]
|
|
||||||
public string AlbumArtist { get; set; }
|
|
||||||
[JsonProperty("song_count")]
|
|
||||||
[NotMapped]
|
|
||||||
public int SongCount { get; set; }
|
|
||||||
[JsonProperty("year")]
|
|
||||||
public int Year { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public List<Song> Songs { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class Artist
|
|
||||||
{
|
|
||||||
[JsonProperty("artist_id")]
|
|
||||||
public int ArtistID { get; set; }
|
|
||||||
[JsonProperty("name")]
|
|
||||||
[Column("Artist")]
|
|
||||||
public string Name { get; set; }
|
|
||||||
[JsonProperty("song_count")]
|
|
||||||
[NotMapped]
|
|
||||||
public int SongCount { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public List<Song> Songs { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class BaseResult
|
|
||||||
{
|
|
||||||
[JsonProperty("message")]
|
|
||||||
public string Message { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class CoverArt
|
|
||||||
{
|
|
||||||
#region Properties
|
|
||||||
[JsonProperty("cover_art_id")]
|
|
||||||
public int CoverArtID { get; set; }
|
|
||||||
[JsonProperty("title")]
|
|
||||||
public string SongTitle { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
public string ImagePath { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public int SongID { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public List<Song> Songs { get; set; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public string GenerateFilename(int flag)
|
|
||||||
{
|
|
||||||
const int length = 25;
|
|
||||||
const string chars = "ABCDEF0123456789";
|
|
||||||
var random = new Random();
|
|
||||||
var filename = new string(Enumerable.Repeat(chars, length).Select(s =>
|
|
||||||
s[random.Next(s.Length)]).ToArray());
|
|
||||||
var extension = ".mp3";
|
|
||||||
|
|
||||||
return (flag == 0) ? filename : $"{filename}{extension}";
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class Genre
|
|
||||||
{
|
|
||||||
[JsonProperty("genre_id")]
|
|
||||||
public int GenreID { get; set; }
|
|
||||||
[JsonProperty("genre")]
|
|
||||||
[Column("Category")]
|
|
||||||
public string GenreName { get; set; }
|
|
||||||
[JsonProperty("song_count")]
|
|
||||||
[NotMapped]
|
|
||||||
public int SongCount { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public List<Song> Songs { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class LoginResult : BaseResult
|
|
||||||
{
|
|
||||||
[JsonProperty("user_id")]
|
|
||||||
public int UserID { get; set; }
|
|
||||||
[JsonProperty("username")]
|
|
||||||
public string Username { get; set; }
|
|
||||||
[JsonProperty("token")]
|
|
||||||
public string Token { get; set; }
|
|
||||||
[JsonProperty("token_type")]
|
|
||||||
public string TokenType { get; set; }
|
|
||||||
[JsonProperty("expiration")]
|
|
||||||
public int Expiration { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class RegisterResult : BaseResult
|
|
||||||
{
|
|
||||||
[JsonProperty("username")]
|
|
||||||
public string Username { get; set; }
|
|
||||||
[JsonProperty("successfully_registered")]
|
|
||||||
public bool SuccessfullyRegistered { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class Song
|
|
||||||
{
|
|
||||||
#region Properties
|
|
||||||
[JsonProperty("song_id")]
|
|
||||||
public int SongID { get; set; }
|
|
||||||
[JsonProperty("title")]
|
|
||||||
public string Title { get; set; }
|
|
||||||
[JsonProperty("album")]
|
|
||||||
[Column("Album")]
|
|
||||||
public string AlbumTitle { get; set; }
|
|
||||||
[JsonProperty("artist")]
|
|
||||||
public string Artist { get; set; }
|
|
||||||
[JsonProperty("album_artist")]
|
|
||||||
public string AlbumArtist { get; set; }
|
|
||||||
[JsonProperty("year")]
|
|
||||||
public int? Year { get; set; }
|
|
||||||
[JsonProperty("genre")]
|
|
||||||
public string Genre { get; set; }
|
|
||||||
[JsonProperty("duration")]
|
|
||||||
public int Duration { get; set; }
|
|
||||||
[JsonProperty("filename")]
|
|
||||||
public string Filename { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
public string SongDirectory { get; set; }
|
|
||||||
[JsonProperty("track")]
|
|
||||||
public int Track { get; set; } = 0;
|
|
||||||
[JsonProperty("track_count")]
|
|
||||||
public int TrackCount { get; set; } = 0;
|
|
||||||
[JsonProperty("disc")]
|
|
||||||
public int Disc { get; set; } = 0;
|
|
||||||
[JsonProperty("disc_count")]
|
|
||||||
public int DiscCount { get; set; } = 0;
|
|
||||||
[JsonIgnore]
|
|
||||||
public int? AlbumID { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
public int? ArtistID { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
public int? GenreID { get; set; }
|
|
||||||
[JsonIgnore]
|
|
||||||
public int? CoverArtID { get; set; }
|
|
||||||
[JsonProperty("date_created")]
|
|
||||||
public DateTime DateCreated { get; set; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Constructors
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public string SongPath()
|
|
||||||
{
|
|
||||||
var fullPath = SongDirectory;
|
|
||||||
|
|
||||||
if (fullPath[fullPath.Length -1] != '/')
|
|
||||||
{
|
|
||||||
fullPath += "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
fullPath += Filename;
|
|
||||||
|
|
||||||
return fullPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GenerateFilename(int flag = 0)
|
|
||||||
{
|
|
||||||
const int length = 25;
|
|
||||||
const string chars = "ABCDEF0123456789";
|
|
||||||
var random = new Random();
|
|
||||||
var filename = new string(Enumerable.Repeat(chars, length).Select(s =>
|
|
||||||
s[random.Next(s.Length)]).ToArray());
|
|
||||||
var extension = ".mp3";
|
|
||||||
|
|
||||||
return flag == 0 ? filename : $"{filename}{extension}";
|
|
||||||
}
|
|
||||||
public async Task<string> GenerateFilenameAsync(int flag = 0)
|
|
||||||
{
|
|
||||||
const int length = 25;
|
|
||||||
const string chars = "ABCDEF0123456789";
|
|
||||||
var extension = ".mp3";
|
|
||||||
var random = new Random();
|
|
||||||
var filename = await Task.Run(() =>
|
|
||||||
{
|
|
||||||
return new string(Enumerable.Repeat(chars, length).Select(s =>
|
|
||||||
s[random.Next(s.Length)]).ToArray());
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
return flag == 0 ? filename : $"{filename}{extension}";
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class SongData
|
|
||||||
{
|
|
||||||
public int ID { get; set; }
|
|
||||||
public byte[] Data { get; set; }
|
|
||||||
public int SongID { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class SongResult
|
|
||||||
{
|
|
||||||
[JsonProperty("message")]
|
|
||||||
public string Message { get; set; }
|
|
||||||
[JsonProperty("song_title")]
|
|
||||||
public string SongTitle { get; set; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
public class Token
|
|
||||||
{
|
|
||||||
#region Properties
|
|
||||||
[JsonProperty("scope")]
|
|
||||||
public string Scope { get; set; }
|
|
||||||
[JsonProperty("exp")]
|
|
||||||
public int Expiration { get; set; }
|
|
||||||
[JsonProperty("aud")]
|
|
||||||
public string Audience { get; set; }
|
|
||||||
[JsonProperty("iss")]
|
|
||||||
public string Issuer { get; set; }
|
|
||||||
[JsonProperty("iat")]
|
|
||||||
public int Issued { get; set; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public bool TokenExpired()
|
|
||||||
{
|
|
||||||
var result = false;
|
|
||||||
var currentDate = DateTime.Now;
|
|
||||||
|
|
||||||
var currentDateInSeconds = Math.Floor((currentDate - DateTime.UnixEpoch).TotalSeconds);
|
|
||||||
|
|
||||||
result = (currentDateInSeconds >= Expiration) ? true : false;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool ContainsScope(string desiredScope)
|
|
||||||
{
|
|
||||||
var result = false;
|
|
||||||
result = Scope.Contains(desiredScope);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Erroneous()
|
|
||||||
{
|
|
||||||
var result = true;
|
|
||||||
Func<string, bool> isEmpty = a => string.IsNullOrEmpty(a);
|
|
||||||
result = (!isEmpty(Scope) && !isEmpty(Audience) && !isEmpty(Issuer) &&
|
|
||||||
Expiration > 0 && Issued > 0) ? false : true;
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
|
|
||||||
namespace Icarus.Models
|
|
||||||
{
|
|
||||||
[Table("User")]
|
|
||||||
public class User
|
|
||||||
{
|
|
||||||
#region Properties
|
|
||||||
[JsonProperty("user_id")]
|
|
||||||
[Column("UserID")]
|
|
||||||
[Key]
|
|
||||||
public int UserID { get; set; }
|
|
||||||
[JsonProperty("username")]
|
|
||||||
public string Username { get; set; }
|
|
||||||
[JsonProperty("password")]
|
|
||||||
public string Password { get; set; }
|
|
||||||
[JsonProperty("email")]
|
|
||||||
public string Email { get; set; }
|
|
||||||
[JsonProperty("phone")]
|
|
||||||
[Column("Phone")]
|
|
||||||
public string Phone { get; set; }
|
|
||||||
[JsonProperty("firstname")]
|
|
||||||
public string Firstname { get; set; }
|
|
||||||
[JsonProperty("lastname")]
|
|
||||||
public string Lastname { get; set; }
|
|
||||||
[JsonProperty("email_verified")]
|
|
||||||
[NotMapped]
|
|
||||||
public bool EmailVerified { get; set; }
|
|
||||||
[JsonProperty("date_created")]
|
|
||||||
public DateTime DateCreated { get; set; }
|
|
||||||
[JsonProperty("status")]
|
|
||||||
public string Status { get; set; }
|
|
||||||
[JsonProperty("last_login")]
|
|
||||||
public DateTime? LastLogin { get; set; }
|
|
||||||
|
|
||||||
[JsonIgnore]
|
|
||||||
[NotMapped]
|
|
||||||
public System.Collections.Generic.IEnumerable<string> Roles { get; set; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
public System.Collections.Generic.IEnumerable<System.Security.Claims.Claim> Claims()
|
|
||||||
{
|
|
||||||
var claims = new System.Collections.Generic.List<System.Security.Claims.Claim> { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, Username) };
|
|
||||||
claims.AddRange(Roles.Select(role => new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Role, role)));
|
|
||||||
|
|
||||||
return claims;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using NLog.Web;
|
|
||||||
|
|
||||||
namespace Icarus
|
|
||||||
{
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static void Main(string[] args)
|
|
||||||
{
|
|
||||||
var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
logger.Debug("init main");
|
|
||||||
|
|
||||||
CreateHostBuilder(args).Build().Run();
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
logger.Error(ex, "An error occurred");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
NLog.LogManager.Shutdown();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IHostBuilder CreateHostBuilder(string[] args) =>
|
|
||||||
Host.CreateDefaultBuilder(args)
|
|
||||||
.ConfigureWebHostDefaults(webBuilder =>
|
|
||||||
{
|
|
||||||
webBuilder.UseStartup<Startup>();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
|
||||||
"iisSettings": {
|
|
||||||
"windowsAuthentication": false,
|
|
||||||
"anonymousAuthentication": true,
|
|
||||||
"iisExpress": {
|
|
||||||
"applicationUrl": "http://localhost:20781",
|
|
||||||
"sslPort": 44394
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"profiles": {
|
|
||||||
"IIS Express": {
|
|
||||||
"commandName": "IISExpress",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Icarus": {
|
|
||||||
"commandName": "Project",
|
|
||||||
"launchBrowser": true,
|
|
||||||
"dotnetRuneMessage": true,
|
|
||||||
"launchUrl": "swagger",
|
|
||||||
"applicationUrl": "http://localhost:5002",
|
|
||||||
"environmentVariables": {
|
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +1,17 @@
|
|||||||
# Icarus
|
|
||||||
|
|
||||||
Icarus is a music streaming API Server that interacts with [Mear](https://github.com/amazing-username/mear).
|
|
||||||
|
|
||||||
### Interfacing With Icarus
|
|
||||||
|
|
||||||
One can interface with Icarus the music server either by:
|
|
||||||
|
|
||||||
* [Mear](https://github.com/amazing-username/mear) - Partially implemented (under development)
|
|
||||||
* [IcarusDownloadManager](https://github.com/amazing-username/IcarusDownloadManager)
|
|
||||||
|
|
||||||
|
|
||||||
## Built With
|
# Getting Started
|
||||||
|
## Docker
|
||||||
|
Make sure `icarus_auth` is located in the root of the parent directory if using docker.
|
||||||
|
|
||||||
* C# [.NET](https://dotnet.microsoft.com/) 6
|
Create a `.env` file for both projects - `icarus_auth` and `icarus` - in the root of each project.
|
||||||
* [MySql](https://www.nuget.org/packages/MySql.Data/)
|
|
||||||
* OpenSSL
|
|
||||||
* BCrypt.Net-Next
|
|
||||||
* DotNetZip
|
|
||||||
* ID3
|
|
||||||
* JWT
|
|
||||||
* Microsoft.AspNetCore.Authentication.JwtBearer
|
|
||||||
* Microsoft.AspNetCore.Mvc.NewtonsoftJson
|
|
||||||
* Microsoft.EntityFrameworkCore
|
|
||||||
* Microsoft.EntityFrameworkCore.Tools
|
|
||||||
* MySql.EntityFrameworkCore
|
|
||||||
* [Newtonsoft.Json](https://www.newtonsoft.com/json)
|
|
||||||
* NLog.Web.AspNetCpre
|
|
||||||
* Portable.BouncyCastle
|
|
||||||
* RestSharp
|
|
||||||
* SevenZip
|
|
||||||
* System.IdentityModel.Tokens.Jwt
|
|
||||||
* [TagLib#](https://github.com/mono/taglib-sharp)
|
|
||||||
|
|
||||||
|
Build containers
|
||||||
|
```
|
||||||
## Getting started
|
docker compose build --ssh default api auth_api
|
||||||
|
|
||||||
There are several things that need to be completed to properly setup and secure the API.
|
|
||||||
This API uses OpenAPI Specification 3.0. After configuring the API, launch the software
|
|
||||||
and navigate your browser to https://localhost:5001/swagger to view the endpoints.
|
|
||||||
|
|
||||||
1. JWT Information
|
|
||||||
2. API filesystem paths
|
|
||||||
3. Database connection string
|
|
||||||
4. Migrations
|
|
||||||
|
|
||||||
|
|
||||||
### JWT Information
|
|
||||||
|
|
||||||
Configure JWT information. Notably the Secret
|
|
||||||
|
|
||||||
```Json
|
|
||||||
"JWT": {
|
|
||||||
"Issuer": "IcarusAPI",
|
|
||||||
"Audience": "IcarusAPIClient",
|
|
||||||
"Secret": "Manaiswhatyouthinkitis",
|
|
||||||
"Subject": "Authorization"
|
|
||||||
},
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Bring it up
|
||||||
Replace [domain] with the domain name that represent's your domain. Replace [identifier] with the identifer root name in the appsettings environment file. Not the friendly name but the root name of the identifier, omitting the http protocol and the *api* path.
|
|
||||||
|
|
||||||
```Json
|
|
||||||
"Auth0": {
|
|
||||||
"Domain": "[domain].auth0.com",
|
|
||||||
"ApiIdentifier": "https://[identifier]/api"
|
|
||||||
},
|
|
||||||
```
|
```
|
||||||
|
docker compose up -d --force-recreate api auth_api
|
||||||
**Note**: The Auth0 section is likely to be changed or removed in future releases.
|
|
||||||
|
|
||||||
|
|
||||||
### API filesystem paths
|
|
||||||
|
|
||||||
For the purposes of properly uploading, downloading, updating, deleting, and streaming songs the API filesystem paths must be configured. What is meant by this is that the `RootMusicPath` directory where all music will be stored must exist as well as the `ArchivePath` and `TemporaryMusicPath` paths. An example on a Linux system:
|
|
||||||
```Json
|
|
||||||
{
|
|
||||||
"RootMusicPath": "/home/dev/null/music/",
|
|
||||||
"TemporaryMusicPath": "/home/dev/null/music/temp/",
|
|
||||||
"ArchivePath": "/home/dev/null/music/archive/",
|
|
||||||
"CoverArtPath": "/home/dev/null/music/coverart/"
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
* RootMusicPath - Where music will be stored in the following convention: *`Artist/Album/Songs`*
|
|
||||||
* TemporaryMusicPath - Where music will be stored when uploding songs to the server until the metadata has been fully parsed and entered into the database. Upon completion the files will be deleted and moved to the appropriate path in the `RootMusicPath`
|
|
||||||
* ArchivePath - When downloading compressed songs this is the path where songs will be compressed prior to dataa being read into memory, deleting the compressed file, and sending the compressed file from memory to the client
|
|
||||||
* CoverArtPath - Root directory where cover art will be saved to
|
|
||||||
|
|
||||||
|
|
||||||
**Note**: The `TemporaryMusic` or `ArchivePath` does not have to be located in the `RootMusicPath`. Ensure that the permissions are properly set for all of the paths.
|
|
||||||
|
|
||||||
### Database connection string
|
|
||||||
|
|
||||||
In order for Database functionality to be operable, there must be a valid connection string and credentials with appropriate permissions. **At the moment there is only support for MySQL**. Depending on your environment `Release` or `Debug` you will need to edit the appsettings.json or appsettings.Development.json accordingly. An example of the fields to change are below:
|
|
||||||
|
|
||||||
```Json
|
|
||||||
{
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Server=localhost;Database=my_db;Uid=admin;Pwd=toughpassword;"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Server - The address or domain name of the MySQL server
|
|
||||||
* Database - The database name
|
|
||||||
* Uid - Username
|
|
||||||
* Password - Self-explanatory
|
|
||||||
|
|
||||||
The only requirement of the User is that the user should have full permissions to the database as well as permissions to create a database. Other than that, that is all that is required.
|
|
||||||
|
|
||||||
### Migrations
|
|
||||||
|
|
||||||
Prior to starting the API, the Migrations must be applied. There are 6 tables with migrations being applied and thy are:
|
|
||||||
* Users
|
|
||||||
* Song
|
|
||||||
* Album
|
|
||||||
* Artist
|
|
||||||
* Year
|
|
||||||
* Genre
|
|
||||||
|
|
||||||
There is a script for Linux systems to apply these migrations, it can be found in the [Scripts/Migrations/Linux](https://github.com/amazing-username/Icarus/blob/master/Scripts/Migrations/Linux/AddUpdate.sh) directory. Just merely execute:
|
|
||||||
```shell
|
|
||||||
scripts/Migrations/Linux/AddUpdate.sh
|
|
||||||
```
|
|
||||||
Or you can manually add the migrations like so for each migration:
|
|
||||||
```shell
|
|
||||||
dotnet ef migrations Add [Migration] --context [Migration]Context
|
|
||||||
```
|
|
||||||
Then update the migrations to the database like so<sup>*</sup>:
|
|
||||||
```shell
|
|
||||||
dotnet ef database update --context [Migration]Context
|
|
||||||
```
|
|
||||||
From this point the database has been successfully configured. Metadata and song filesystem locations can be saved.
|
|
||||||
|
|
||||||
<sup>*</sup> Will only need to execute this for UserContext and SongContext because the Song table has relational constraints with Album, Artist, Year, and Genre.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on the code of conduct, and the process for submitting pull requests to the project.
|
|
||||||
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
echo "Adding migrations..."
|
|
||||||
echo "Adding User migration"
|
|
||||||
dotnet ef migrations add User --context UserContext
|
|
||||||
echo "Adding Song migration"
|
|
||||||
dotnet ef migrations add Song --context SongContext
|
|
||||||
echo "Adding Album migration"
|
|
||||||
dotnet ef migrations add Album --context AlbumContext
|
|
||||||
echo "Adding Artist migration"
|
|
||||||
dotnet ef migrations add Artist --context ArtistContext
|
|
||||||
echo "Adding Genre migration"
|
|
||||||
dotnet ef migrations add Genre --context GenreContext
|
|
||||||
echo "Adding Year migration"
|
|
||||||
dotnet ef migrations add Year --context YearContext
|
|
||||||
echo "Adding Cover art migration"
|
|
||||||
dotnet ef migrations add CoverArt --context CoverArtContext
|
|
||||||
|
|
||||||
echo "Updating migrations.."
|
|
||||||
echo "Updating User migration"
|
|
||||||
dotnet ef database update --context UserContext
|
|
||||||
echo "Updating Song migration"
|
|
||||||
echo "Updating Album migration"
|
|
||||||
echo "Updating Artist migration"
|
|
||||||
echo "Updating Genre migration"
|
|
||||||
echo "Updating Year migration"
|
|
||||||
echo "Updating Cover art migration"
|
|
||||||
dotnet ef database update --context SongContext
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
CREATE DATABASE Icarus;
|
|
||||||
|
|
||||||
USE Icarus;
|
|
||||||
|
|
||||||
CREATE TABLE CoverArt (
|
|
||||||
CoverArtID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
SongTitle TEXT NOT NULL,
|
|
||||||
ImagePath TEXT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (CoverArtID)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Album (
|
|
||||||
AlbumID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
Title TEXT NOT NULL,
|
|
||||||
Artist TEXT NOT NULL,
|
|
||||||
Year INT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (AlbumID)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Artist (
|
|
||||||
ArtistID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
Artist TEXT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (ArtistID)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Genre (
|
|
||||||
GenreID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
Category TEXT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (GenreID)
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
CREATE TABLE Song (
|
|
||||||
SongID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
Title TEXT NOT NULL,
|
|
||||||
Artist TEXT NOT NULL,
|
|
||||||
Album TEXT NOT NULL,
|
|
||||||
AlbumArtist TEXT NOT NULL,
|
|
||||||
Genre TEXT NOT NULL,
|
|
||||||
Year INT NOT NULL,
|
|
||||||
Duration INT NOT NULL,
|
|
||||||
Track INT NOT NULL,
|
|
||||||
TrackCount INT NOT NULL,
|
|
||||||
Disc INT NOT NULL,
|
|
||||||
DiscCount INT NOT NULL,
|
|
||||||
SongDirectory TEXT NOT NULL,
|
|
||||||
Filename TEXT NOT NULL,
|
|
||||||
CoverArtID INT NOT NULL,
|
|
||||||
ArtistID INT NOT NULL,
|
|
||||||
AlbumID INT NOT NULL,
|
|
||||||
GenreID INT NOT NULL,
|
|
||||||
DateCreated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
PRIMARY KEY (SongID),
|
|
||||||
CONSTRAINT FK_Song_CoverArtID FOREIGN KEY (CoverArtID) REFERENCES CoverArt (CoverArtID),
|
|
||||||
CONSTRAINT FK_Song_ArtistID FOREIGN KEY (ArtistID) REFERENCES Artist (ArtistID),
|
|
||||||
CONSTRAINT FK_Song_AlbumID FOREIGN KEY (AlbumID) REFERENCES Album (AlbumID),
|
|
||||||
CONSTRAINT FK_Song_GenreID FOREIGN KEY (GenreID) REFERENCES Genre (GenreID)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE User (
|
|
||||||
UserID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
Firstname TEXT NOT NULL,
|
|
||||||
Lastname TEXT NOT NULL,
|
|
||||||
Email TEXT NOT NULL,
|
|
||||||
Phone TEXT NOT NULL,
|
|
||||||
Username TEXT NOT NULL,
|
|
||||||
Password TEXT NOT NULL,
|
|
||||||
DateCreated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
Status TEXT DEFAULT 'Active',
|
|
||||||
LastLogin DATETIME NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (UserID)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Salt (
|
|
||||||
SaltID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
Salt TEXT NOT NULL,
|
|
||||||
UserID INT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (SaltID),
|
|
||||||
CONSTRAINT FK_Salt_UserID FOREIGN KEY (UserID) REFERENCES User (UserID)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE Token (
|
|
||||||
TokenID INT NOT NULL AUTO_INCREMENT,
|
|
||||||
AccessToken TEXT NOT NULL,
|
|
||||||
TokenType TEXT NOT NULL,
|
|
||||||
Expires DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
Issued DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
UserID INT NOT NULL,
|
|
||||||
|
|
||||||
PRIMARY KEY (TokenID),
|
|
||||||
CONSTRAINT FK_Token_UserID FOREIGN KEY (UserID) REFERENCES User (UserID)
|
|
||||||
);
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
delete from Song;
|
|
||||||
delete from Album;
|
|
||||||
delete from Artist;
|
|
||||||
delete from Genre;
|
|
||||||
delete from CoverArt;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
select * from Song;
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
drop database Icarus;
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
|
||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using Microsoft.IdentityModel.Tokens;
|
|
||||||
using Microsoft.OpenApi.Models;
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using NLog;
|
|
||||||
using NLog.Web;
|
|
||||||
using NLog.Web.AspNetCore;
|
|
||||||
|
|
||||||
using Icarus.Database.Contexts;
|
|
||||||
|
|
||||||
namespace Icarus
|
|
||||||
{
|
|
||||||
public class Startup
|
|
||||||
{
|
|
||||||
#region Constructors
|
|
||||||
public Startup(IConfiguration configuration)
|
|
||||||
{
|
|
||||||
Configuration = configuration;
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Properties
|
|
||||||
public IConfiguration Configuration { get; }
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
|
|
||||||
#region Methods
|
|
||||||
// This method gets called by the runtime. Use this method to add services to the container.
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
|
||||||
{
|
|
||||||
services.AddControllers();
|
|
||||||
|
|
||||||
var auth_id = Configuration["Auth0:Domain"];
|
|
||||||
var domain = $"https://{auth_id}/";
|
|
||||||
var audience = Configuration["Auth0:ApiIdentifier"];
|
|
||||||
|
|
||||||
var connString = Configuration.GetConnectionString("DefaultConnection");
|
|
||||||
|
|
||||||
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
|
|
||||||
{
|
|
||||||
options.RequireHttpsMetadata = false;
|
|
||||||
options.SaveToken = true;
|
|
||||||
options.TokenValidationParameters = new TokenValidationParameters()
|
|
||||||
{
|
|
||||||
ValidateIssuer = true,
|
|
||||||
ValidateAudience = true,
|
|
||||||
ValidAudience = Configuration["JWT:Audience"],
|
|
||||||
ValidIssuer = Configuration["JWT:Issuer"],
|
|
||||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["JWT:Secret"]))
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
services.AddDbContext<SongContext>(options => options.UseMySQL(connString));
|
|
||||||
services.AddDbContext<AlbumContext>(options => options.UseMySQL(connString));
|
|
||||||
services.AddDbContext<ArtistContext>(options => options.UseMySQL(connString));
|
|
||||||
services.AddDbContext<UserContext>(options => options.UseMySQL(connString));
|
|
||||||
services.AddDbContext<GenreContext>(options => options.UseMySQL(connString));
|
|
||||||
services.AddDbContext<CoverArtContext>(options => options.UseMySQL(connString));
|
|
||||||
|
|
||||||
services.AddControllers()
|
|
||||||
.AddNewtonsoftJson();
|
|
||||||
|
|
||||||
services.AddEndpointsApiExplorer();
|
|
||||||
services.AddSwaggerGen(c =>
|
|
||||||
{
|
|
||||||
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
|
|
||||||
|
|
||||||
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Icarus", Version = "v1" });
|
|
||||||
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme()
|
|
||||||
{
|
|
||||||
Name = "Authorization",
|
|
||||||
Scheme = "Bearer",
|
|
||||||
BearerFormat = "JWT",
|
|
||||||
Type = SecuritySchemeType.ApiKey,
|
|
||||||
In = ParameterLocation.Header,
|
|
||||||
Description = "Bearer *Auth Token*",
|
|
||||||
});
|
|
||||||
c.AddSecurityRequirement(new OpenApiSecurityRequirement
|
|
||||||
{
|
|
||||||
{
|
|
||||||
new OpenApiSecurityScheme
|
|
||||||
{
|
|
||||||
Reference = new OpenApiReference
|
|
||||||
{
|
|
||||||
Type = ReferenceType.SecurityScheme,
|
|
||||||
Id = "Bearer"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
new string[] {}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Called by the runtime. Use this method to configure the HTTP request pipeline.
|
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
|
||||||
{
|
|
||||||
// NOTE: Dev-related configuration can be done when env.IsDevelopment() evaluated to true
|
|
||||||
if (env.IsDevelopment())
|
|
||||||
{
|
|
||||||
app.UseSwagger();
|
|
||||||
app.UseSwaggerUI();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseRouting();
|
|
||||||
app.UseAuthentication();
|
|
||||||
app.UseAuthorization();
|
|
||||||
app.UseEndpoints(endpoints =>
|
|
||||||
{
|
|
||||||
endpoints.MapControllers();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Icarus.Types
|
|
||||||
{
|
|
||||||
public enum CoverArtField
|
|
||||||
{
|
|
||||||
SongTitle = 0,
|
|
||||||
ImagePath
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Icarus.Types
|
|
||||||
{
|
|
||||||
public enum DirectoryType
|
|
||||||
{
|
|
||||||
Music = 0,
|
|
||||||
CoverArt
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Debug",
|
|
||||||
"System": "Information",
|
|
||||||
"Microsoft": "Information"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Auth0": {
|
|
||||||
"Domain": "[domain].auth0.com",
|
|
||||||
"ApiIdentifier": "https://[identifier]/api",
|
|
||||||
"ClientId": "",
|
|
||||||
"ClientSecret": ""
|
|
||||||
},
|
|
||||||
"RSAKeys": {
|
|
||||||
"PrivateKeyPath": "",
|
|
||||||
"PublicKeyPath": ""
|
|
||||||
},
|
|
||||||
"JWT": {
|
|
||||||
"Issuer": "",
|
|
||||||
"Audience": "",
|
|
||||||
"Secret": "",
|
|
||||||
"Subject": ""
|
|
||||||
},
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
|
||||||
},
|
|
||||||
"RootMusicPath": "/music/path/",
|
|
||||||
"TemporaryMusicPath": "/music/temp/path/",
|
|
||||||
"ArchivePath": "/archive/path/",
|
|
||||||
"CoverArtPath": "/cover/art/path/"
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
{
|
|
||||||
"Logging": {
|
|
||||||
"LogLevel": {
|
|
||||||
"Default": "Trace",
|
|
||||||
"Microsoft": "Information"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"AllowedHosts": "*",
|
|
||||||
"Auth0": {
|
|
||||||
"Domain": "[domain].auth0.com",
|
|
||||||
"ApiIdentifier": "https://[identifier]/api",
|
|
||||||
"ClientId": "",
|
|
||||||
"ClientSecret": ""
|
|
||||||
},
|
|
||||||
"RSAKeys": {
|
|
||||||
"PrivateKeyPath": "",
|
|
||||||
"PublicKeyPath": ""
|
|
||||||
},
|
|
||||||
"JWT": {
|
|
||||||
"Issuer": "",
|
|
||||||
"Audience": "",
|
|
||||||
"Secret": "",
|
|
||||||
"Subject": ""
|
|
||||||
},
|
|
||||||
"ConnectionStrings": {
|
|
||||||
"DefaultConnection": "Server=;Database=;Uid=;Pwd=;"
|
|
||||||
},
|
|
||||||
"RootMusicPath": "/music/path/",
|
|
||||||
"TemporaryMusicPath": "/music/temp/path/",
|
|
||||||
"ArchivePath": "/archive/path/",
|
|
||||||
"CoverArtPath": "/cover/art/path/"
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
version: '3.8' # Use a recent version
|
||||||
|
|
||||||
|
services:
|
||||||
|
# --- Web API ---
|
||||||
|
api:
|
||||||
|
build: # Tells docker-compose to build the Dockerfile in the current directory
|
||||||
|
context: .
|
||||||
|
ssh: ["default"] # Uses host's SSH agent
|
||||||
|
container_name: icarus # Optional: Give the container a specific name
|
||||||
|
ports:
|
||||||
|
# Map host port 8000 to container port 3000 (adjust as needed)
|
||||||
|
# Format: "HOST_PORT:CONTAINER_PORT"
|
||||||
|
- "8000:3000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
depends_on:
|
||||||
|
main_db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- main-api-network
|
||||||
|
restart: unless-stopped # Optional: Restart policy
|
||||||
|
|
||||||
|
# --- Web API auth ---
|
||||||
|
auth_api:
|
||||||
|
build:
|
||||||
|
context: ../icarus_auth # IMPORTANT: Relative path to the local checkout of your web API repo (containing the Dockerfile)
|
||||||
|
ssh: ["default"] # Uses host's SSH agent
|
||||||
|
dockerfile: Dockerfile # Optional: Specify if your Dockerfile has a non-standard name
|
||||||
|
container_name: auth_api
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8001:3000" # Map host port 8000 to container port 8000 (adjust container port based on your app's EXPOSE in Dockerfile)
|
||||||
|
# environment:
|
||||||
|
# Environment variables your API needs, e.g., database connection
|
||||||
|
# Add other necessary environment variables
|
||||||
|
env_file:
|
||||||
|
- ../icarus_auth/.env
|
||||||
|
depends_on:
|
||||||
|
auth_db:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- auth-api-network
|
||||||
|
# Optional: Mount local code for development (live reload)
|
||||||
|
# volumes:
|
||||||
|
# - ./path/to/your/web-api-repo:/app
|
||||||
|
|
||||||
|
# PostgreSQL Database Service
|
||||||
|
# --- icarus web api db ---
|
||||||
|
main_db:
|
||||||
|
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller)
|
||||||
|
container_name: icarus_db # Optional: Give the container a specific name
|
||||||
|
environment:
|
||||||
|
# These MUST match the user, password, and database name in the DATABASE_URL above
|
||||||
|
POSTGRES_USER: ${POSTGRES_MAIN_USER:-icarus}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_MAIN_PASSWORD:-password}
|
||||||
|
POSTGRES_DB: ${POSTGRES_MAIN_DB:-icarus_db}
|
||||||
|
volumes:
|
||||||
|
# Persist database data using a named volume
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
|
||||||
|
- "5432:5432"
|
||||||
|
healthcheck:
|
||||||
|
# Checks if Postgres is ready to accept connections
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
restart: always # Optional: Restart policy
|
||||||
|
networks:
|
||||||
|
- main-api-network
|
||||||
|
|
||||||
|
# --- icarus web auth api db ---
|
||||||
|
auth_db:
|
||||||
|
image: postgres:17.5-alpine # Use an official Postgres image (Alpine variant is smaller)
|
||||||
|
container_name: icarus_auth_db # Optional: Give the container a specific name
|
||||||
|
environment:
|
||||||
|
# These MUST match the user, password, and database name in the DATABASE_URL above
|
||||||
|
POSTGRES_USER: ${POSTGRES_AUTH_USER:-icarus_op}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_AUTH_PASSWORD:-password}
|
||||||
|
POSTGRES_DB: ${POSTGRES_AUTH_DB:-icarus_auth_db}
|
||||||
|
volumes:
|
||||||
|
# Persist database data using a named volume
|
||||||
|
- postgres_data_auth:/var/lib/postgresql/data
|
||||||
|
ports:
|
||||||
|
# Optional: Expose port 5432 ONLY if you need to connect directly from your host machine (e.g., for debugging)
|
||||||
|
- "5433:5432"
|
||||||
|
healthcheck:
|
||||||
|
# Checks if Postgres is ready to accept connections
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
restart: always # Optional: Restart policy
|
||||||
|
networks:
|
||||||
|
- auth-api-network
|
||||||
|
|
||||||
|
# Define the named volume for data persistence
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
driver: local # Use the default local driver
|
||||||
|
postgres_data_auth:
|
||||||
|
driver: local # Use the default local driver
|
||||||
|
|
||||||
|
# Define the network (optional, but good practice)
|
||||||
|
networks:
|
||||||
|
main-api-network:
|
||||||
|
driver: bridge
|
||||||
|
auth-api-network:
|
||||||
|
driver: bridge
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
-- Table to store queued songs to process
|
||||||
|
CREATE TABLE IF NOT EXISTS "songQueue" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
status TEXT CHECK (status IN ('pending', 'ready', 'processing', 'done')),
|
||||||
|
data BYTEA NULL,
|
||||||
|
user_id UUID NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table to store queued metadata
|
||||||
|
CREATE TABLE IF NOT EXISTS "metadataQueue" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
metadata jsonb NOT NULL,
|
||||||
|
created_at timestamptz DEFAULT now(),
|
||||||
|
song_queue_id UUID NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table to store queued coverart
|
||||||
|
CREATE TABLE IF NOT EXISTS "coverartQueue" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
data BYTEA NULL,
|
||||||
|
song_queue_id UUID NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create an index for better query performance
|
||||||
|
CREATE INDEX metadata_queue_data_metadata ON "metadataQueue" USING gin (metadata);
|
||||||
|
|
||||||
|
-- Table to store a song's info
|
||||||
|
CREATE TABLE IF NOT EXISTS "song" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
artist TEXT NOT NULL,
|
||||||
|
album_artist TEXT NOT NULL,
|
||||||
|
album TEXT NOT NULL,
|
||||||
|
genre TEXT NOT NULL,
|
||||||
|
-- TODO: Address discrepancy of date and year at some point
|
||||||
|
-- date TEXT NOT NULL,
|
||||||
|
year INT NOT NULL,
|
||||||
|
track INT NOT NULL,
|
||||||
|
disc INT NOT NULL,
|
||||||
|
track_count INT NOT NULL,
|
||||||
|
disc_count INT NOT NULL,
|
||||||
|
duration INT NOT NULL,
|
||||||
|
audio_type TEXT NOT NULL,
|
||||||
|
date_created timestamptz DEFAULT now(),
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
directory TEXT NOT NULL,
|
||||||
|
user_id UUID NULL
|
||||||
|
-- TODO: Add coverart id later. This will allow multiple songs to be linked to a single cover art
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "coverart" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
-- TODO: Separate path later
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
song_id UUID NOT NULL
|
||||||
|
-- TODO: Add type later
|
||||||
|
);
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
|
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
||||||
autoReload="true"
|
|
||||||
internalLogLevel="Info"
|
|
||||||
internalLogFile="Icarus.txt">
|
|
||||||
|
|
||||||
<!-- enable asp.net core layout renderers -->
|
|
||||||
<extensions>
|
|
||||||
<add assembly="NLog.Web.AspNetCore"/>
|
|
||||||
</extensions>
|
|
||||||
|
|
||||||
<!-- the targets to write to -->
|
|
||||||
<targets>
|
|
||||||
<!-- write logs to file -->
|
|
||||||
<target xsi:type="File" name="allfile" fileName="Icarus-${shortdate}.log"
|
|
||||||
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
|
|
||||||
|
|
||||||
<!-- another file log, only own logs. Uses some ASP.NET core renderers -->
|
|
||||||
<target xsi:type="File" name="ownFile-web" fileName="Icarus-${shortdate}.log"
|
|
||||||
layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />
|
|
||||||
</targets>
|
|
||||||
|
|
||||||
<!-- rules to map from logger name to target -->
|
|
||||||
<rules>
|
|
||||||
<!--All logs, including from Microsoft-->
|
|
||||||
<logger name="*" minlevel="Trace" writeTo="allfile" />
|
|
||||||
|
|
||||||
<!--Skip non-critical Microsoft logs and so log only own logs-->
|
|
||||||
<logger name="Microsoft.*" maxlevel="Info" final="true" /> <!-- BlackHole without writeTo -->
|
|
||||||
<logger name="*" minlevel="Trace" writeTo="ownFile-web" />
|
|
||||||
</rules>
|
|
||||||
</nlog>
|
|
||||||
@@ -0,0 +1,563 @@
|
|||||||
|
// TODO: Separate queue and coverart endpoints
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct CoverArtQueue {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod request {
|
||||||
|
|
||||||
|
pub mod link {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Request {
|
||||||
|
pub coverart_id: uuid::Uuid,
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_coverart_no_data {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Params {
|
||||||
|
pub id: Option<uuid::Uuid>,
|
||||||
|
pub song_queue_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_coverart_with_data {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Params {
|
||||||
|
pub id: Option<uuid::Uuid>,
|
||||||
|
pub song_queue_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod create_coverart {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Request {
|
||||||
|
pub song_id: uuid::Uuid,
|
||||||
|
pub coverart_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod wipe_data_from_coverart_queue {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Request {
|
||||||
|
pub coverart_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod link {
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Id {
|
||||||
|
pub coverart_id: uuid::Uuid,
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<Id>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_coverart_no_data {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<super::super::CoverArtQueue>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_coverart_with_data {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod create_coverart {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<icarus_models::coverart::CoverArt>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod wipe_data_from_coverart_queue {
|
||||||
|
#[derive(Debug, Default, serde::Deserialize, serde::Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod db {
|
||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
pub async fn insert(pool: &sqlx::PgPool, data: &Vec<u8>) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "coverartQueue" (data) VALUES($1) RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(data)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let id: uuid::Uuid = row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn update(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
coverart_id: &uuid::Uuid,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<i32, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "coverartQueue" SET song_queue_id = $1 WHERE id = $2;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.bind(coverart_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(_) => Ok(0),
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_queue_with_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<super::CoverArtQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, song_queue_id FROM "coverartQueue" WHERE id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(super::CoverArtQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_queue_id: row
|
||||||
|
.try_get("song_queue_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
}),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_queue_with_song_queue_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<super::CoverArtQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, song_queue_id FROM "coverartQueue" WHERE song_queue_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(super::CoverArtQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_queue_id: row
|
||||||
|
.try_get("song_queue_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
}),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_queue_data_with_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<Vec<u8>, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT data FROM "coverartQueue" WHERE id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e:?}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("data")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_coverart_queue_data_with_song_queue_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<Vec<u8>, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT data FROM "coverartQueue" WHERE song_queue_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error querying data: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("data")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wipe_data(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
coverart_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
UPDATE "coverartQueue" SET data = NULL WHERE id = $1 RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(coverart_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error updating query: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap()),
|
||||||
|
Err(_) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod cov_db {
|
||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
pub async fn create(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
coverart: &icarus_models::coverart::CoverArt,
|
||||||
|
song_id: &uuid::Uuid,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "coverart" (title, path, song_id) VALUES($1, $2, $3) RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&coverart.title)
|
||||||
|
.bind(&coverart.path)
|
||||||
|
.bind(song_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let id: uuid::Uuid = row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod endpoint {
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
|
||||||
|
pub async fn queue(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
mut multipart: axum::extract::Multipart,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::Response::default();
|
||||||
|
|
||||||
|
match multipart.next_field().await {
|
||||||
|
Ok(Some(field)) => {
|
||||||
|
let name = field.name().unwrap().to_string();
|
||||||
|
let file_name = field.file_name().unwrap().to_string();
|
||||||
|
let content_type = field.content_type().unwrap().to_string();
|
||||||
|
let data = field.bytes().await.unwrap();
|
||||||
|
let raw_data = data.to_vec();
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"Received file '{}' (name = '{}', content-type = '{}', size = {})",
|
||||||
|
file_name,
|
||||||
|
name,
|
||||||
|
content_type,
|
||||||
|
data.len()
|
||||||
|
);
|
||||||
|
|
||||||
|
match super::db::insert(&pool, &raw_data).await {
|
||||||
|
Ok(id) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(id);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None) => (axum::http::StatusCode::BAD_REQUEST, axum::Json(response)),
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn link(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::link::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::link::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::link::Response::default();
|
||||||
|
let id = payload.coverart_id;
|
||||||
|
let song_id = payload.song_queue_id;
|
||||||
|
|
||||||
|
match super::db::update(&pool, &id, &song_id).await {
|
||||||
|
Ok(_o) => {
|
||||||
|
response.data.push(super::response::link::Id {
|
||||||
|
song_queue_id: song_id,
|
||||||
|
coverart_id: id,
|
||||||
|
});
|
||||||
|
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_coverart_no_data(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::extract::Query(params): axum::extract::Query<
|
||||||
|
super::request::fetch_coverart_no_data::Params,
|
||||||
|
>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::fetch_coverart_no_data::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::fetch_coverart_no_data::Response::default();
|
||||||
|
|
||||||
|
match params.id {
|
||||||
|
Some(id) => match super::db::get_coverart_queue_with_id(&pool, &id).await {
|
||||||
|
Ok(cover_art_queue) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(cover_art_queue);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => match params.song_queue_id {
|
||||||
|
Some(song_queue_id) => {
|
||||||
|
match super::db::get_coverart_queue_with_song_queue_id(&pool, &song_queue_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(cover_art_queue) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(cover_art_queue);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
response.message = String::from("No valid id provided");
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_coverart_with_data(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||||
|
) -> (axum::http::StatusCode, axum::response::Response) {
|
||||||
|
match super::db::get_coverart_queue_data_with_id(&pool, &id).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let bytes = axum::body::Bytes::from(data);
|
||||||
|
let mut response = bytes.into_response();
|
||||||
|
let headers = response.headers_mut();
|
||||||
|
// TODO: Address this hard coding for the coverart content type
|
||||||
|
headers.insert(axum::http::header::CONTENT_TYPE, "image".parse().unwrap());
|
||||||
|
// TODO: Make the conent disposition more dynamic
|
||||||
|
headers.insert(
|
||||||
|
axum::http::header::CONTENT_DISPOSITION,
|
||||||
|
format!("attachment; filename=\"{id}.jpg\"")
|
||||||
|
.parse()
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
|
||||||
|
(axum::http::StatusCode::OK, response)
|
||||||
|
}
|
||||||
|
Err(_err) => (
|
||||||
|
axum::http::StatusCode::BAD_REQUEST,
|
||||||
|
axum::response::Response::default(),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn create_coverart(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::create_coverart::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::create_coverart::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::create_coverart::Response::default();
|
||||||
|
let id = payload.coverart_queue_id;
|
||||||
|
|
||||||
|
match super::db::get_coverart_queue_data_with_id(&pool, &id).await {
|
||||||
|
Ok(data) => {
|
||||||
|
let song_id = payload.song_id;
|
||||||
|
match crate::callers::song::song_db::get_song(&pool, &song_id).await {
|
||||||
|
Ok(song) => {
|
||||||
|
let directory = icarus_envy::environment::get_root_directory().await;
|
||||||
|
let dir = std::path::Path::new(&directory);
|
||||||
|
|
||||||
|
// TODO: Make this random and the file extension should not be hard coded
|
||||||
|
let filename = format!("{}-coverart.jpeg", &song.filename[..8]);
|
||||||
|
let save_path = dir.join(&filename);
|
||||||
|
let path = String::from(save_path.to_str().unwrap());
|
||||||
|
let mut coverart =
|
||||||
|
icarus_models::coverart::init::init_coverart_only_path(path);
|
||||||
|
coverart.title = song.album.clone();
|
||||||
|
coverart.data = data;
|
||||||
|
|
||||||
|
let mut file = std::fs::File::create(&save_path).unwrap();
|
||||||
|
file.write_all(&coverart.data).unwrap();
|
||||||
|
|
||||||
|
match super::cov_db::create(&pool, &coverart, &song.id).await {
|
||||||
|
Ok(id) => {
|
||||||
|
coverart.id = id;
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(coverart);
|
||||||
|
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn wipe_data_from_coverart_queue(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::Json(payload): axum::Json<super::request::wipe_data_from_coverart_queue::Request>,
|
||||||
|
) -> (
|
||||||
|
axum::http::StatusCode,
|
||||||
|
axum::Json<super::response::wipe_data_from_coverart_queue::Response>,
|
||||||
|
) {
|
||||||
|
let mut response = super::response::wipe_data_from_coverart_queue::Response::default();
|
||||||
|
let coverart_queue_id = payload.coverart_queue_id;
|
||||||
|
|
||||||
|
match super::db::get_coverart_queue_with_id(&pool, &coverart_queue_id).await {
|
||||||
|
Ok(coverart_queue) => match super::db::wipe_data(&pool, &coverart_queue.id).await {
|
||||||
|
Ok(id) => {
|
||||||
|
response.message = String::from("Success");
|
||||||
|
response.data.push(id);
|
||||||
|
(axum::http::StatusCode::OK, axum::Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::BAD_REQUEST, axum::Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(axum::http::StatusCode::NOT_FOUND, axum::Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
// TODO: Explicitly make this module target queueing a song's metadata
|
||||||
|
pub mod request {
|
||||||
|
|
||||||
|
pub mod queue_metadata {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Deserialize, Serialize, sqlx::FromRow)]
|
||||||
|
pub struct Request {
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
pub album: String,
|
||||||
|
pub album_artist: String,
|
||||||
|
pub artist: String,
|
||||||
|
pub disc: i32,
|
||||||
|
pub disc_count: i32,
|
||||||
|
pub duration: i64,
|
||||||
|
pub genre: String,
|
||||||
|
pub title: String,
|
||||||
|
pub track: i32,
|
||||||
|
pub track_count: i32,
|
||||||
|
pub year: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Request {
|
||||||
|
pub async fn to_json_value(&self) -> serde_json::Value {
|
||||||
|
serde_json::json!(
|
||||||
|
{
|
||||||
|
"song_queue_id": &self.song_queue_id,
|
||||||
|
"album": &self.album,
|
||||||
|
"album_artist": &self.album_artist,
|
||||||
|
"genre": &self.genre,
|
||||||
|
"year": &self.year,
|
||||||
|
"track_count": &self.track_count,
|
||||||
|
"disc_count": &self.disc_count,
|
||||||
|
"title": &self.title,
|
||||||
|
"artist": &self.artist,
|
||||||
|
"disc": &self.disc,
|
||||||
|
"track": &self.track,
|
||||||
|
"duration": &self.duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_metadata {
|
||||||
|
#[derive(
|
||||||
|
Debug, Default, serde::Deserialize, serde::Serialize, sqlx::FromRow, sqlx::Decode,
|
||||||
|
)]
|
||||||
|
pub struct Params {
|
||||||
|
pub id: Option<uuid::Uuid>,
|
||||||
|
pub song_queue_id: Option<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
|
||||||
|
pub mod queue_metadata {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<uuid::Uuid>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod fetch_metadata {
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
#[derive(Default, Deserialize, Serialize)]
|
||||||
|
pub struct Response {
|
||||||
|
pub message: String,
|
||||||
|
pub data: Vec<crate::callers::metadata::metadata_queue::MetadataQueue>,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod metadata_queue {
|
||||||
|
use sqlx::Row;
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Serialize, sqlx::FromRow)]
|
||||||
|
pub struct InsertedData {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize, serde::Serialize, sqlx::FromRow)]
|
||||||
|
pub struct MetadataQueue {
|
||||||
|
pub id: uuid::Uuid,
|
||||||
|
pub metadata: serde_json::Value,
|
||||||
|
#[serde(with = "time::serde::rfc3339")]
|
||||||
|
pub created_at: time::OffsetDateTime,
|
||||||
|
pub song_queue_id: uuid::Uuid,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn insert(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
metadata: &serde_json::Value,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<uuid::Uuid, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
INSERT INTO "metadataQueue" (metadata, song_queue_id) VALUES($1, $2) RETURNING id;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(metadata)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let id: uuid::Uuid = row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_with_song_queue_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
song_queue_id: &uuid::Uuid,
|
||||||
|
) -> Result<MetadataQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT * FROM "metadataQueue" WHERE song_queue_id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(song_queue_id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => Ok(MetadataQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
metadata: row
|
||||||
|
.try_get("metadata")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
created_at: row
|
||||||
|
.try_get("created_at")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_queue_id: row
|
||||||
|
.try_get("song_queue_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
}),
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_with_id(
|
||||||
|
pool: &sqlx::PgPool,
|
||||||
|
id: &uuid::Uuid,
|
||||||
|
) -> Result<MetadataQueue, sqlx::Error> {
|
||||||
|
let result = sqlx::query(
|
||||||
|
r#"
|
||||||
|
SELECT id, metadata, created_at, song_queue_id FROM "metadataQueue" WHERE id = $1;
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_one(pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
eprintln!("Error inserting: {e}");
|
||||||
|
});
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(row) => {
|
||||||
|
let data: serde_json::Value = row
|
||||||
|
.try_get("metadata")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap();
|
||||||
|
Ok(MetadataQueue {
|
||||||
|
id: row
|
||||||
|
.try_get("id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
metadata: data,
|
||||||
|
created_at: row
|
||||||
|
.try_get("created_at")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
song_queue_id: row
|
||||||
|
.try_get("song_queue_id")
|
||||||
|
.map_err(|_e| sqlx::Error::RowNotFound)
|
||||||
|
.unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
Err(_err) => Err(sqlx::Error::RowNotFound),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod endpoint {
|
||||||
|
use axum::{Json, http::StatusCode};
|
||||||
|
|
||||||
|
pub async fn queue_metadata(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
Json(payload): Json<super::request::queue_metadata::Request>,
|
||||||
|
) -> (StatusCode, Json<super::response::queue_metadata::Response>) {
|
||||||
|
let mut results: Vec<uuid::Uuid> = Vec::new();
|
||||||
|
let mut response = super::response::queue_metadata::Response::default();
|
||||||
|
let meta = payload.to_json_value().await;
|
||||||
|
match super::metadata_queue::insert(&pool, &meta, &payload.song_queue_id).await {
|
||||||
|
Ok(metadata_queue_id) => {
|
||||||
|
results.push(metadata_queue_id);
|
||||||
|
response.data = results;
|
||||||
|
response.message = if response.data.is_empty() {
|
||||||
|
String::from("Error")
|
||||||
|
} else {
|
||||||
|
String::from("Success")
|
||||||
|
};
|
||||||
|
|
||||||
|
(StatusCode::OK, Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(StatusCode::BAD_REQUEST, Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn fetch_metadata(
|
||||||
|
axum::Extension(pool): axum::Extension<sqlx::PgPool>,
|
||||||
|
axum::extract::Query(params): axum::extract::Query<super::request::fetch_metadata::Params>,
|
||||||
|
) -> (StatusCode, Json<super::response::fetch_metadata::Response>) {
|
||||||
|
let mut response = super::response::fetch_metadata::Response::default();
|
||||||
|
|
||||||
|
match params.id {
|
||||||
|
Some(id) => {
|
||||||
|
println!("Something works {id}");
|
||||||
|
|
||||||
|
match super::metadata_queue::get_with_id(&pool, &id).await {
|
||||||
|
Ok(item) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(item);
|
||||||
|
(StatusCode::OK, Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(StatusCode::BAD_REQUEST, Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => match params.song_queue_id {
|
||||||
|
Some(song_queue_id) => {
|
||||||
|
println!("Song queue Id is probably not nil");
|
||||||
|
match super::metadata_queue::get_with_song_queue_id(&pool, &song_queue_id).await
|
||||||
|
{
|
||||||
|
Ok(item) => {
|
||||||
|
response.message = String::from("Successful");
|
||||||
|
response.data.push(item);
|
||||||
|
(StatusCode::OK, Json(response))
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
response.message = err.to_string();
|
||||||
|
(StatusCode::BAD_REQUEST, Json(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
println!("What is going on?");
|
||||||
|
(StatusCode::BAD_REQUEST, Json(response))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
pub mod coverart;
|
||||||
|
pub mod metadata;
|
||||||
|
pub mod song;
|
||||||
|
|
||||||
|
pub mod endpoints {
|
||||||
|
pub const QUEUESONG: &str = "/api/v2/song/queue";
|
||||||
|
pub const QUEUESONGLINKUSERID: &str = "/api/v2/song/queue/link";
|
||||||
|
pub const QUEUESONGDATA: &str = "/api/v2/song/queue/{id}";
|
||||||
|
pub const QUEUESONGUPDATE: &str = "/api/v2/song/queue/{id}";
|
||||||
|
pub const NEXTQUEUESONG: &str = "/api/v2/song/queue/next";
|
||||||
|
pub const QUEUEMETADATA: &str = "/api/v2/song/metadata/queue";
|
||||||
|
pub const QUEUECOVERART: &str = "/api/v2/coverart/queue";
|
||||||
|
pub const QUEUECOVERARTDATA: &str = "/api/v2/coverart/queue/data/{id}";
|
||||||
|
pub const QUEUECOVERARTLINK: &str = "/api/v2/coverart/queue/link";
|
||||||
|
pub const QUEUESONGDATAWIPE: &str = "/api/v2/song/queue/data/wipe";
|
||||||
|
pub const QUEUECOVERARTDATAWIPE: &str = "/api/v2/coverart/queue/data/wipe";
|
||||||
|
|
||||||
|
pub const CREATESONG: &str = "/api/v2/song";
|
||||||
|
pub const GETSONGS: &str = "/api/v2/song";
|
||||||
|
pub const CREATECOVERART: &str = "/api/v2/coverart";
|
||||||
|
}
|
||||||
|
|
||||||
|
pub mod response {
|
||||||
|
pub const SUCCESSFUL: &str = "SUCCESSFUL";
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
|
||||||
|
-- Table to store queued songs to process
|
||||||
|
CREATE TABLE IF NOT EXISTS "songQueue" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
status TEXT CHECK (status IN ('pending', 'ready', 'processing', 'done')),
|
||||||
|
data BYTEA NULL,
|
||||||
|
user_id UUID NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table to store queued metadata
|
||||||
|
CREATE TABLE IF NOT EXISTS "metadataQueue" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
metadata jsonb NOT NULL,
|
||||||
|
created_at timestamptz DEFAULT now(),
|
||||||
|
song_queue_id UUID NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Table to store queued coverart
|
||||||
|
CREATE TABLE IF NOT EXISTS "coverartQueue" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
data BYTEA NULL,
|
||||||
|
song_queue_id UUID NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create an index for better query performance
|
||||||
|
CREATE INDEX metadata_queue_data_metadata ON "metadataQueue" USING gin (metadata);
|
||||||
|
|
||||||
|
-- Table to store a song's info
|
||||||
|
CREATE TABLE IF NOT EXISTS "song" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
artist TEXT NOT NULL,
|
||||||
|
album_artist TEXT NOT NULL,
|
||||||
|
album TEXT NOT NULL,
|
||||||
|
genre TEXT NOT NULL,
|
||||||
|
year INT NOT NULL,
|
||||||
|
track INT NOT NULL,
|
||||||
|
disc INT NOT NULL,
|
||||||
|
track_count INT NOT NULL,
|
||||||
|
disc_count INT NOT NULL,
|
||||||
|
duration INT NOT NULL,
|
||||||
|
audio_type TEXT NOT NULL,
|
||||||
|
date_created timestamptz DEFAULT now(),
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
directory TEXT NOT NULL,
|
||||||
|
user_id UUID NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS "coverart" (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
song_id UUID NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Might not to disable constraints on fields
|
||||||
|
-- INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('44cf7940-34ff-489f-9124-d0ec90a55af9', 'Hypocrite Like The Rest', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 1, 1, 9, 1, 139, 'flac', '2020-01-01 13:00:00-05', 'track01.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670');
|
||||||
|
-- Re-enable the constraints on the fields
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- Add migration script here
|
||||||
|
INSERT INTO "song" (id, title, artist, album_artist, album, genre, year, track, disc, track_count, disc_count, duration, audio_type, date_created, filename, directory, user_id) VALUES('44cf7940-34ff-489f-9124-d0ec90a55af9', 'Hypocrite Like The Rest', 'Kuoth', 'Kuoth', 'I', 'Alternative Hip-Hop', 2020, 1, 1, 9, 1, 139, 'flac', '2020-01-01 13:00:00-05', 'track01.flac', 'tests/I', '47491f9b-725a-4ba4-b9a5-711e1be46670');
|
||||||
|
INSERT INTO "coverart" VALUES('996122cd-5ae9-4013-9934-60768d3006ed', 'I', 'tests/I/Coverart.jpg', '44cf7940-34ff-489f-9124-d0ec90a55af9');
|
||||||
|
After Width: | Height: | Size: 6.7 MiB |
@@ -0,0 +1,73 @@
|
|||||||
|
{
|
||||||
|
"album": "I",
|
||||||
|
"album_artist": "Kuoth",
|
||||||
|
"disc_count": 1,
|
||||||
|
"genre": "Alternative Hip-Hop",
|
||||||
|
"year": 2020,
|
||||||
|
"track_count": 9,
|
||||||
|
"tracks": [
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Hypocrite Like the Rest",
|
||||||
|
"duration": 139,
|
||||||
|
"track": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "It's Too Late",
|
||||||
|
"duration": 116,
|
||||||
|
"track": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Hell Might be Nice",
|
||||||
|
"duration": 229,
|
||||||
|
"track": 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Dark Side of Progress",
|
||||||
|
"duration": 187,
|
||||||
|
"track": 4
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Distractions",
|
||||||
|
"duration": 141,
|
||||||
|
"track": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "But You Won't Listen",
|
||||||
|
"duration": 184,
|
||||||
|
"track": 6
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Guidance",
|
||||||
|
"duration": 121,
|
||||||
|
"track": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "M",
|
||||||
|
"duration": 98,
|
||||||
|
"track": 8
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"artist": "Kuoth",
|
||||||
|
"disc": 1,
|
||||||
|
"title": "Social Acceptance",
|
||||||
|
"duration": 169,
|
||||||
|
"track": 9
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||