Compare commits
No commits in common. "rewrite/service-repo" and "main" have entirely different histories.
rewrite/se
...
main
94 changed files with 1835 additions and 4432 deletions
|
|
@ -9,13 +9,13 @@ import (
|
||||||
|
|
||||||
"arimelody-web/admin/templates"
|
"arimelody-web/admin/templates"
|
||||||
"arimelody-web/controller"
|
"arimelody-web/controller"
|
||||||
|
"arimelody-web/log"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
func accountHandler(app *app.AppState) http.Handler {
|
func accountHandler(app *model.AppState) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/account/totp-setup", totpSetupHandler(app))
|
mux.Handle("/account/totp-setup", totpSetupHandler(app))
|
||||||
|
|
@ -28,7 +28,7 @@ func accountHandler(app *app.AppState) http.Handler {
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
func accountIndexHandler(app *app.AppState) http.Handler {
|
func accountIndexHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ func accountIndexHandler(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func changePasswordHandler(app *app.AppState) http.Handler {
|
func changePasswordHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -107,7 +107,8 @@ func changePasswordHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.AccountService.ChangePassword(session.Account.ID, string(hashedPassword))
|
session.Account.Password = string(hashedPassword)
|
||||||
|
err = controller.UpdateAccount(app.DB, session.Account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to update account password: %v\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to update account password: %v\n", err)
|
||||||
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
|
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
|
||||||
|
|
@ -115,7 +116,7 @@ func changePasswordHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r))
|
app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" changed password by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r))
|
||||||
|
|
||||||
controller.SetSessionError(app.DB, session, "")
|
controller.SetSessionError(app.DB, session, "")
|
||||||
controller.SetSessionMessage(app.DB, session, "Password updated successfully.")
|
controller.SetSessionMessage(app.DB, session, "Password updated successfully.")
|
||||||
|
|
@ -123,7 +124,7 @@ func changePasswordHandler(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteAccountHandler(app *app.AppState) http.Handler {
|
func deleteAccountHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -145,13 +146,13 @@ func deleteAccountHandler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
// check password
|
// check password
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil {
|
if err := bcrypt.CompareHashAndPassword([]byte(session.Account.Password), []byte(r.Form.Get("password"))); err != nil {
|
||||||
app.LogService.Warn(model.LOG_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r))
|
app.Log.Warn(log.TYPE_ACCOUNT, "Account \"%s\" attempted account deletion with incorrect password. (%s)", session.Account.Username, controller.ResolveIP(app, r))
|
||||||
controller.SetSessionError(app.DB, session, "Incorrect password.")
|
controller.SetSessionError(app.DB, session, "Incorrect password.")
|
||||||
http.Redirect(w, r, "/admin/account", http.StatusFound)
|
http.Redirect(w, r, "/admin/account", http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.AccountService.Delete(session.Account.ID)
|
err = controller.DeleteAccount(app.DB, session.Account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Failed to delete account: %v\n", err)
|
||||||
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
|
controller.SetSessionError(app.DB, session, "Something went wrong. Please try again.")
|
||||||
|
|
@ -159,7 +160,7 @@ func deleteAccountHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r))
|
app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" deleted by user request. (%s)", session.Account.Username, controller.ResolveIP(app, r))
|
||||||
|
|
||||||
controller.SetSessionAccount(app.DB, session, nil)
|
controller.SetSessionAccount(app.DB, session, nil)
|
||||||
controller.SetSessionError(app.DB, session, "")
|
controller.SetSessionError(app.DB, session, "")
|
||||||
|
|
@ -175,7 +176,7 @@ type totpConfirmData struct {
|
||||||
QRBase64Image string
|
QRBase64Image string
|
||||||
}
|
}
|
||||||
|
|
||||||
func totpSetupHandler(app *app.AppState) http.Handler {
|
func totpSetupHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodGet {
|
if r.Method == http.MethodGet {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
@ -246,7 +247,7 @@ func totpSetupHandler(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func totpConfirmHandler(app *app.AppState) http.Handler {
|
func totpConfirmHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -310,7 +311,7 @@ func totpConfirmHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" created TOTP method \"%s\".", session.Account.Username, totp.Name)
|
app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" created TOTP method \"%s\".", session.Account.Username, totp.Name)
|
||||||
|
|
||||||
controller.SetSessionError(app.DB, session, "")
|
controller.SetSessionError(app.DB, session, "")
|
||||||
controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" created successfully.", totp.Name))
|
controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" created successfully.", totp.Name))
|
||||||
|
|
@ -318,7 +319,7 @@ func totpConfirmHandler(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func totpDeleteHandler(app *app.AppState) http.Handler {
|
func totpDeleteHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -358,7 +359,7 @@ func totpDeleteHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" deleted TOTP method \"%s\".", session.Account.Username, totp.Name)
|
app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" deleted TOTP method \"%s\".", session.Account.Username, totp.Name)
|
||||||
|
|
||||||
controller.SetSessionError(app.DB, session, "")
|
controller.SetSessionError(app.DB, session, "")
|
||||||
controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" deleted successfully.", totp.Name))
|
controller.SetSessionMessage(app.DB, session, fmt.Sprintf("TOTP method \"%s\" deleted successfully.", totp.Name))
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/admin/templates"
|
"arimelody-web/admin/templates"
|
||||||
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveArtists(app *app.AppState) http.Handler {
|
func serveArtists(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ func serveArtists(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
artists, err := app.MusicService.GetAllArtists()
|
artists, err := controller.GetAllArtists(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch artists: %s\n", err)
|
fmt.Printf("WARN: Failed to fetch artists: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -45,11 +45,11 @@ func serveArtists(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveArtist(app *app.AppState, artistID string) http.Handler {
|
func serveArtist(app *model.AppState, artistID string) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
artist, err := app.MusicService.GetArtistByID(artistID)
|
artist, err := controller.GetArtist(app.DB, artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if artist == nil {
|
if artist == nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -60,7 +60,7 @@ func serveArtist(app *app.AppState, artistID string) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
credits, err := app.MusicService.GetArtistCredits(artistID, true)
|
credits, err := controller.GetArtistCredits(app.DB, artist.ID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err)
|
fmt.Printf("WARN: Failed to serve admin artist page for %s: %s\n", artistID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
|
||||||
134
admin/http.go
134
admin/http.go
|
|
@ -2,6 +2,7 @@ package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -10,8 +11,8 @@ import (
|
||||||
|
|
||||||
"arimelody-web/admin/templates"
|
"arimelody-web/admin/templates"
|
||||||
"arimelody-web/controller"
|
"arimelody-web/controller"
|
||||||
|
"arimelody-web/log"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
|
||||||
"arimelody-web/view"
|
"arimelody-web/view"
|
||||||
|
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
@ -22,7 +23,7 @@ type adminPageData struct {
|
||||||
Session *model.Session
|
Session *model.Session
|
||||||
}
|
}
|
||||||
|
|
||||||
func Handler(app *app.AppState) http.Handler {
|
func Handler(app *model.AppState) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/qr-test", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -74,7 +75,7 @@ func Handler(app *app.AppState) http.Handler {
|
||||||
return enforceSession(app, mux)
|
return enforceSession(app, mux)
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdminIndexHandler(app *app.AppState) http.Handler {
|
func AdminIndexHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path != "/" {
|
if r.URL.Path != "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -83,39 +84,39 @@ func AdminIndexHandler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
releases, err := app.MusicService.GetAllReleases(false, 3)
|
releases, err := controller.GetAllReleases(app.DB, false, 3, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
releaseCount, err := app.MusicService.GetReleaseCount(false)
|
releaseCount, err := controller.GetReleaseCount(app.DB, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to pull releases count: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
artists, err := app.MusicService.GetAllArtists()
|
artists, err := controller.GetAllArtists(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artists: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
artistCount, err := app.MusicService.GetArtistCount()
|
artistCount, err := controller.GetArtistCount(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %s\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to pull artist count: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks, err := app.MusicService.GetOrphanTracks()
|
tracks, err := controller.GetOrphanTracks(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to pull orphan tracks: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
trackCount, err := app.MusicService.GetTrackCount()
|
trackCount, err := controller.GetTrackCount(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %s\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to pull track count: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -149,7 +150,7 @@ func AdminIndexHandler(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerAccountHandler(app *app.AppState) http.Handler {
|
func registerAccountHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
|
@ -222,13 +223,13 @@ func registerAccountHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultAvatar := "/img/default-avatar.png"
|
account := model.Account{
|
||||||
accountID, err := app.AccountService.Create(
|
Username: credentials.Username,
|
||||||
credentials.Username,
|
Password: string(hashedPassword),
|
||||||
string(hashedPassword),
|
Email: sql.NullString{ String: credentials.Email, Valid: true },
|
||||||
&credentials.Email,
|
AvatarURL: sql.NullString{ String: "/img/default-avatar.png", Valid: true },
|
||||||
&defaultAvatar,
|
}
|
||||||
)
|
err = controller.CreateAccount(app.DB, &account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.HasPrefix(err.Error(), "pq: duplicate key") {
|
if strings.HasPrefix(err.Error(), "pq: duplicate key") {
|
||||||
controller.SetSessionError(app.DB, session, "An account with that username already exists.")
|
controller.SetSessionError(app.DB, session, "An account with that username already exists.")
|
||||||
|
|
@ -241,36 +242,22 @@ func registerAccountHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(
|
app.Log.Info(log.TYPE_ACCOUNT, "Account \"%s\" (%s) created using invite \"%s\". (%s)", account.Username, account.ID, invite.Code, controller.ResolveIP(app, r))
|
||||||
model.LOG_ACCOUNT,
|
|
||||||
"Account \"%s\" (%s) created using invite \"%s\". (%s)",
|
|
||||||
credentials.Username,
|
|
||||||
accountID,
|
|
||||||
invite.Code,
|
|
||||||
controller.ResolveIP(app, r),
|
|
||||||
)
|
|
||||||
|
|
||||||
err = controller.DeleteInvite(app.DB, invite.Code)
|
err = controller.DeleteInvite(app.DB, invite.Code)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.LogService.Warn(model.LOG_ACCOUNT, "Failed to delete expired invite \"%s\": %v", invite.Code, err)
|
app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete expired invite \"%s\": %v", invite.Code, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// registration success!
|
// registration success!
|
||||||
if account, err := app.AccountService.GetByID(accountID); err != nil || account == nil {
|
controller.SetSessionAccount(app.DB, session, &account)
|
||||||
controller.SetSessionError(
|
|
||||||
app.DB, session,
|
|
||||||
"Account created, but something went wrong logging you in. Please try logging in manually.",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
controller.SetSessionAccount(app.DB, session, account)
|
|
||||||
}
|
|
||||||
controller.SetSessionMessage(app.DB, session, "")
|
controller.SetSessionMessage(app.DB, session, "")
|
||||||
controller.SetSessionError(app.DB, session, "")
|
controller.SetSessionError(app.DB, session, "")
|
||||||
http.Redirect(w, r, "/admin", http.StatusFound)
|
http.Redirect(w, r, "/admin", http.StatusFound)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginHandler(app *app.AppState) http.Handler {
|
func loginHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -312,7 +299,7 @@ func loginHandler(app *app.AppState) http.Handler {
|
||||||
username := r.FormValue("username")
|
username := r.FormValue("username")
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account for login: %v\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch account for login: %v\n", err)
|
||||||
controller.SetSessionError(app.DB, session, "Invalid username or password.")
|
controller.SetSessionError(app.DB, session, "Invalid username or password.")
|
||||||
|
|
@ -332,7 +319,7 @@ func loginHandler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password))
|
err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.LogService.Warn(model.LOG_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r))
|
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" attempted login with incorrect password. (%s)", account.Username, controller.ResolveIP(app, r))
|
||||||
if locked := handleFailedLogin(app, account, r); locked {
|
if locked := handleFailedLogin(app, account, r); locked {
|
||||||
controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
|
controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -366,8 +353,8 @@ func loginHandler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
// login success!
|
// login success!
|
||||||
// TODO: log login activity to user
|
// TODO: log login activity to user
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(app, r))
|
app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in. (%s)", account.Username, controller.ResolveIP(app, r))
|
||||||
app.LogService.Warn(model.LOG_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username)
|
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" does not have any TOTP methods assigned.", account.Username)
|
||||||
|
|
||||||
err = controller.SetSessionAccount(app.DB, session, account)
|
err = controller.SetSessionAccount(app.DB, session, account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -382,7 +369,7 @@ func loginHandler(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginTOTPHandler(app *app.AppState) http.Handler {
|
func loginTOTPHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
|
@ -420,7 +407,7 @@ func loginTOTPHandler(app *app.AppState) http.Handler {
|
||||||
totpCode := r.FormValue("totp")
|
totpCode := r.FormValue("totp")
|
||||||
|
|
||||||
if len(totpCode) != controller.TOTP_CODE_LENGTH {
|
if len(totpCode) != controller.TOTP_CODE_LENGTH {
|
||||||
app.LogService.Warn(model.LOG_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
|
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Invalid TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
|
||||||
controller.SetSessionError(app.DB, session, "Invalid TOTP.")
|
controller.SetSessionError(app.DB, session, "Invalid TOTP.")
|
||||||
render()
|
render()
|
||||||
return
|
return
|
||||||
|
|
@ -434,7 +421,7 @@ func loginTOTPHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if totpMethod == nil {
|
if totpMethod == nil {
|
||||||
app.LogService.Warn(model.LOG_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
|
app.Log.Warn(log.TYPE_ACCOUNT, "\"%s\" failed login (Incorrect TOTP). (%s)", session.AttemptAccount.Username, controller.ResolveIP(app, r))
|
||||||
if locked := handleFailedLogin(app, session.AttemptAccount, r); locked {
|
if locked := handleFailedLogin(app, session.AttemptAccount, r); locked {
|
||||||
controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
|
controller.SetSessionError(app.DB, session, "Too many failed attempts. This account is now locked.")
|
||||||
controller.SetSessionAttemptAccount(app.DB, session, nil)
|
controller.SetSessionAttemptAccount(app.DB, session, nil)
|
||||||
|
|
@ -446,7 +433,7 @@ func loginTOTPHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(app, r))
|
app.Log.Info(log.TYPE_ACCOUNT, "\"%s\" logged in with TOTP method \"%s\". (%s)", session.AttemptAccount.Username, totpMethod.Name, controller.ResolveIP(app, r))
|
||||||
|
|
||||||
err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount)
|
err = controller.SetSessionAccount(app.DB, session, session.AttemptAccount)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -465,7 +452,7 @@ func loginTOTPHandler(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutHandler(app *app.AppState) http.Handler {
|
func logoutHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -527,7 +514,7 @@ func staticHandler() http.Handler {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func enforceSession(app *app.AppState, next http.Handler) http.Handler {
|
func enforceSession(app *model.AppState, next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session, err := controller.GetSessionFromRequest(app, r)
|
session, err := controller.GetSessionFromRequest(app, r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -560,45 +547,8 @@ func enforceSession(app *app.AppState, next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper for handling login failures. Increments the account auth failure
|
func handleFailedLogin(app *model.AppState, account *model.Account, r *http.Request) bool {
|
||||||
// count and logs warnings. If failure count exceeds MAX_LOGIN_FAIL_ATTEMPTS,
|
locked, err := controller.IncrementAccountFails(app.DB, account.ID)
|
||||||
// the account will be locked.
|
|
||||||
func handleFailedLogin(app *app.AppState, account *model.Account, r *http.Request) bool {
|
|
||||||
failAttempts, err := app.AccountService.IncrementFails(account.ID)
|
|
||||||
if failAttempts >= model.MAX_LOGIN_FAIL_ATTEMPTS {
|
|
||||||
err = app.AccountService.Lock(account.ID)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintf(
|
|
||||||
os.Stderr,
|
|
||||||
"WARN: Failed to lock account \"%s\": %v\n",
|
|
||||||
account.Username,
|
|
||||||
err,
|
|
||||||
)
|
|
||||||
app.LogService.Warn(
|
|
||||||
model.LOG_ACCOUNT,
|
|
||||||
"Failed to lock account \"%s\"",
|
|
||||||
account.Username,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Fprintf(
|
|
||||||
os.Stderr,
|
|
||||||
"Account \"%s\" was locked: %d failed login attempts (IP: %s)",
|
|
||||||
account.Username,
|
|
||||||
model.MAX_LOGIN_FAIL_ATTEMPTS,
|
|
||||||
controller.ResolveIP(app, r),
|
|
||||||
)
|
|
||||||
app.LogService.Warn(
|
|
||||||
model.LOG_ACCOUNT,
|
|
||||||
"Account \"%s\" was locked: %d failed login attempts (IP: %s)",
|
|
||||||
account.Username,
|
|
||||||
model.MAX_LOGIN_FAIL_ATTEMPTS,
|
|
||||||
controller.ResolveIP(app, r),
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(
|
fmt.Fprintf(
|
||||||
os.Stderr,
|
os.Stderr,
|
||||||
|
|
@ -606,12 +556,20 @@ func handleFailedLogin(app *app.AppState, account *model.Account, r *http.Reques
|
||||||
account.Username,
|
account.Username,
|
||||||
err,
|
err,
|
||||||
)
|
)
|
||||||
app.LogService.Warn(
|
app.Log.Warn(
|
||||||
model.LOG_ACCOUNT,
|
log.TYPE_ACCOUNT,
|
||||||
"Failed to increment login failures for \"%s\"",
|
"Failed to increment login failures for \"%s\"",
|
||||||
account.Username,
|
account.Username,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if locked {
|
||||||
return false
|
app.Log.Warn(
|
||||||
|
log.TYPE_ACCOUNT,
|
||||||
|
"Account \"%s\" was locked: %d failed login attempts (IP: %s)",
|
||||||
|
account.Username,
|
||||||
|
model.MAX_LOGIN_FAIL_ATTEMPTS,
|
||||||
|
controller.ResolveIP(app, r),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return locked
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,15 @@ package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/admin/templates"
|
"arimelody-web/admin/templates"
|
||||||
|
"arimelody-web/log"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func logsHandler(app *app.AppState) http.Handler {
|
func logsHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodGet {
|
if r.Method != http.MethodGet {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -19,16 +19,16 @@ func logsHandler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
levelFilter := []model.LogLevel{}
|
levelFilter := []log.LogLevel{}
|
||||||
typeFilter := []string{}
|
typeFilter := []string{}
|
||||||
|
|
||||||
query := r.URL.Query().Get("q")
|
query := r.URL.Query().Get("q")
|
||||||
|
|
||||||
for key, value := range r.URL.Query() {
|
for key, value := range r.URL.Query() {
|
||||||
if strings.HasPrefix(key, "level-") && value[0] == "on" {
|
if strings.HasPrefix(key, "level-") && value[0] == "on" {
|
||||||
m := map[string]model.LogLevel{
|
m := map[string]log.LogLevel{
|
||||||
"info": model.LEVEL_INFO,
|
"info": log.LEVEL_INFO,
|
||||||
"warn": model.LEVEL_WARN,
|
"warn": log.LEVEL_WARN,
|
||||||
}
|
}
|
||||||
level, ok := m[strings.TrimPrefix(key, "level-")]
|
level, ok := m[strings.TrimPrefix(key, "level-")]
|
||||||
if ok {
|
if ok {
|
||||||
|
|
@ -43,7 +43,7 @@ func logsHandler(app *app.AppState) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logs, err := app.LogService.Search(levelFilter, typeFilter, query, 100, 0)
|
logs, err := app.Log.Search(levelFilter, typeFilter, query, 100, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch audit logs: %v\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch audit logs: %v\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -52,7 +52,7 @@ func logsHandler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
type LogsResponse struct {
|
type LogsResponse struct {
|
||||||
adminPageData
|
adminPageData
|
||||||
Logs []*model.Log
|
Logs []*log.Log
|
||||||
}
|
}
|
||||||
|
|
||||||
err = templates.LogsTemplate.Execute(w, LogsResponse{
|
err = templates.LogsTemplate.Execute(w, LogsResponse{
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/admin/templates"
|
"arimelody-web/admin/templates"
|
||||||
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
|
||||||
"arimelody-web/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveReleases(app *app.AppState) http.Handler {
|
func serveReleases(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
|
@ -34,7 +33,7 @@ func serveReleases(app *app.AppState) http.Handler {
|
||||||
Releases []*model.Release
|
Releases []*model.Release
|
||||||
}
|
}
|
||||||
|
|
||||||
releases, err := app.MusicService.GetAllFullReleases(false, 0)
|
releases, err := controller.GetAllReleases(app.DB, false, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to fetch releases: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -56,17 +55,13 @@ func serveReleases(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveRelease(app *app.AppState, releaseID string, action string) http.Handler {
|
func serveRelease(app *model.AppState, releaseID string, action string) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
release, err := app.MusicService.GetFullReleaseByID(releaseID)
|
release, err := controller.GetRelease(app.DB, releaseID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -108,7 +103,9 @@ func serveRelease(app *app.AppState, releaseID string, action string) http.Handl
|
||||||
Release *model.Release
|
Release *model.Release
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
for i, track := range release.Tracks {
|
||||||
|
track.Number = i + 1
|
||||||
|
}
|
||||||
|
|
||||||
err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
|
err = templates.EditReleaseTemplate.Execute(w, ReleaseResponse{
|
||||||
adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
|
adminPageData: adminPageData{ Path: r.URL.Path, Session: session },
|
||||||
|
|
@ -132,9 +129,9 @@ func serveEditCredits(release *model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveAddCredit(app *app.AppState, release *model.Release) http.Handler {
|
func serveAddCredit(app *model.AppState, release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
artists, err := app.MusicService.GetArtistsNotOnRelease(release.ID)
|
artists, err := controller.GetArtistsNotOnRelease(app.DB, release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch artists not on %s: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to fetch artists not on %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -158,11 +155,10 @@ func serveAddCredit(app *app.AppState, release *model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveNewCredit(app *app.AppState) http.Handler {
|
func serveNewCredit(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
split := strings.Split(r.URL.Path, "/")
|
artistID := strings.Split(r.URL.Path, "/")[3]
|
||||||
artistID := split[len(split) - 1]
|
artist, err := controller.GetArtist(app.DB, artistID)
|
||||||
artist, err := app.MusicService.GetArtistByID(artistID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err)
|
fmt.Printf("WARN: Failed to fetch artist %s: %s\n", artistID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -199,8 +195,6 @@ func serveEditTracks(release *model.Release) http.Handler {
|
||||||
|
|
||||||
type editTracksData struct { Release *model.Release }
|
type editTracksData struct { Release *model.Release }
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
|
||||||
|
|
||||||
err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release })
|
err := templates.EditTracksTemplate.Execute(w, editTracksData{ Release: release })
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve edit tracks component for %s: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to serve edit tracks component for %s: %s\n", release.ID, err)
|
||||||
|
|
@ -209,9 +203,9 @@ func serveEditTracks(release *model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveAddTrack(app *app.AppState, release *model.Release) http.Handler {
|
func serveAddTrack(app *model.AppState, release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
tracks, err := app.MusicService.GetTracksNotOnRelease(release.ID)
|
tracks, err := controller.GetTracksNotOnRelease(app.DB, release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch tracks not on %s: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to fetch tracks not on %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -235,11 +229,10 @@ func serveAddTrack(app *app.AppState, release *model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveNewTrack(app *app.AppState) http.Handler {
|
func serveNewTrack(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
split := strings.Split(r.URL.Path, "/")
|
trackID := strings.Split(r.URL.Path, "/")[3]
|
||||||
trackID := split[len(split) - 1]
|
track, err := controller.GetTrack(app.DB, trackID)
|
||||||
track, err := app.MusicService.GetTrackByID(trackID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err)
|
fmt.Printf("WARN: Failed to fetch track %s: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ body {
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--fg-0);
|
color: var(--fg-0);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
@ -252,6 +252,12 @@ a {
|
||||||
transition: color .1s ease-out, background-color .1s ease-out;
|
transition: color .1s ease-out, background-color .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
img.icon {
|
img.icon {
|
||||||
height: .8em;
|
height: .8em;
|
||||||
transition: filter .1s ease-out;
|
transition: filter .1s ease-out;
|
||||||
|
|
@ -277,7 +283,7 @@ code {
|
||||||
.card {
|
.card {
|
||||||
flex-basis: 40em;
|
flex-basis: 40em;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: var(--shadow-lg);
|
box-shadow: var(--shadow-lg);
|
||||||
|
|
||||||
|
|
@ -355,7 +361,7 @@ a.delete:not(.button) {
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
|
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 10em;
|
border-radius: 10em;
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
@ -374,27 +380,27 @@ button:active, .button:active {
|
||||||
|
|
||||||
.button.new, button.new {
|
.button.new, button.new {
|
||||||
color: var(--col-on-new);
|
color: var(--col-on-new);
|
||||||
background-color: var(--col-new);
|
background: var(--col-new);
|
||||||
}
|
}
|
||||||
.button.save, button.save {
|
.button.save, button.save {
|
||||||
color: var(--col-on-save);
|
color: var(--col-on-save);
|
||||||
background-color: var(--col-save);
|
background: var(--col-save);
|
||||||
}
|
}
|
||||||
.button.delete, button.delete {
|
.button.delete, button.delete {
|
||||||
color: var(--col-on-delete);
|
color: var(--col-on-delete);
|
||||||
background-color: var(--col-delete);
|
background: var(--col-delete);
|
||||||
}
|
}
|
||||||
.button:hover, button:hover {
|
.button:hover, button:hover {
|
||||||
color: var(--bg-3);
|
color: var(--bg-3);
|
||||||
background-color: var(--fg-3);
|
background: var(--fg-3);
|
||||||
}
|
}
|
||||||
.button:active, button:active {
|
.button:active, button:active {
|
||||||
color: var(--bg-2);
|
color: var(--bg-2);
|
||||||
background-color: var(--fg-0);
|
background: var(--fg-0);
|
||||||
}
|
}
|
||||||
.button[disabled], button[disabled] {
|
.button[disabled], button[disabled] {
|
||||||
color: var(--fg-0) !important;
|
color: var(--fg-0) !important;
|
||||||
background-color: var(--bg-3) !important;
|
background: var(--bg-3) !important;
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
cursor: default !important;
|
cursor: default !important;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
padding: .5em;
|
padding: .5em;
|
||||||
|
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.artist:hover {
|
.artist:hover {
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
text-decoration: hover;
|
text-decoration: hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,29 +4,4 @@ document.addEventListener("readystatechange", () => {
|
||||||
document.querySelectorAll(".artists-group .artist").forEach(el => {
|
document.querySelectorAll(".artists-group .artist").forEach(el => {
|
||||||
hijackClickEvent(el, el.querySelector("a.artist-name"))
|
hijackClickEvent(el, el.querySelector("a.artist-name"))
|
||||||
});
|
});
|
||||||
|
|
||||||
const newArtistBtn = document.getElementById("create-artist");
|
|
||||||
if (newArtistBtn) newArtistBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const id = prompt("Enter an ID for this artist:");
|
|
||||||
if (id == null || id == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/artist", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({id})
|
|
||||||
}).then(res => {
|
|
||||||
res.text().then(text => {
|
|
||||||
if (res.ok) {
|
|
||||||
location = "/admin/artists/" + id;
|
|
||||||
} else {
|
|
||||||
alert(text);
|
|
||||||
console.error(text);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create artist. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ form#delete-account input {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,11 +50,18 @@ input[type="text"] {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
input[type="text"]:hover {
|
||||||
|
border-color: #80808080;
|
||||||
|
}
|
||||||
|
input[type="text"]:active,
|
||||||
|
input[type="text"]:focus {
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
.artist-actions {
|
.artist-actions {
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|
@ -77,7 +84,7 @@ input[type="text"] {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
@ -85,7 +92,7 @@ input[type="text"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
.credit:hover {
|
.credit:hover {
|
||||||
background-color: var(--bg-1);
|
background: var(--bg-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-artwork {
|
.release-artwork {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ input[type="text"] {
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -33,7 +33,7 @@ input[type="text"] {
|
||||||
.release-artwork #remove-artwork {
|
.release-artwork #remove-artwork {
|
||||||
margin-top: .5em;
|
margin-top: .5em;
|
||||||
padding: .3em .6em;
|
padding: .3em .6em;
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-info {
|
.release-info {
|
||||||
|
|
@ -62,13 +62,13 @@ input[type="text"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
#title:hover {
|
#title:hover {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
border-color: var(--fg-0);
|
border-color: var(--fg-0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#title:active,
|
#title:active,
|
||||||
#title:focus {
|
#title:focus {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.release-title small {
|
.release-title small {
|
||||||
|
|
@ -93,7 +93,7 @@ input[type="text"] {
|
||||||
.release-info table tr td:not(:first-child) select:hover,
|
.release-info table tr td:not(:first-child) select:hover,
|
||||||
.release-info table tr td:not(:first-child) input:hover,
|
.release-info table tr td:not(:first-child) input:hover,
|
||||||
.release-info table tr td:not(:first-child) textarea:hover {
|
.release-info table tr td:not(:first-child) textarea:hover {
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
.release-info table td select,
|
.release-info table td select,
|
||||||
|
|
@ -127,7 +127,7 @@ input[type="text"] {
|
||||||
.release-actions button,
|
.release-actions button,
|
||||||
.release-actions .button {
|
.release-actions .button {
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
dialog {
|
||||||
|
|
@ -234,7 +234,7 @@ dialog div.dialog-actions {
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -280,7 +280,7 @@ dialog div.dialog-actions {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
color: var(--fg-2);
|
color: var(--fg-2);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
}
|
}
|
||||||
#editcredits .credit .credit-info .credit-attribute input[type="checkbox"] {
|
#editcredits .credit .credit-info .credit-attribute input[type="checkbox"] {
|
||||||
margin: 0 .3em;
|
margin: 0 .3em;
|
||||||
|
|
@ -299,7 +299,6 @@ dialog div.dialog-actions {
|
||||||
#editcredits .credit .delete {
|
#editcredits .credit .delete {
|
||||||
margin-right: .5em;
|
margin-right: .5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
overflow: visible;
|
|
||||||
}
|
}
|
||||||
#editcredits .credit .delete:hover {
|
#editcredits .credit .delete:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|
@ -316,17 +315,14 @@ dialog div.dialog-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background-color: var(--bg-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit ul li.new-artist:nth-child(even) {
|
#addcredit ul li.new-artist:nth-child(even) {
|
||||||
background: #f0f0f0;
|
background: #f0f0f0;
|
||||||
background-color: var(--bg-1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit ul li.new-artist:hover {
|
#addcredit ul li.new-artist:hover {
|
||||||
background: #e0e0e0;
|
background: #e0e0e0;
|
||||||
background-color: var(--bg-2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#addcredit .new-artist .artist-id {
|
#addcredit .new-artist .artist-id {
|
||||||
|
|
@ -379,8 +375,6 @@ dialog div.dialog-actions {
|
||||||
|
|
||||||
#editlinks tr {
|
#editlinks tr {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: var(--bg-1);
|
|
||||||
transition: background-color .1s ease-out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks th {
|
#editlinks th {
|
||||||
|
|
@ -391,7 +385,7 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks tr:nth-child(odd) {
|
#editlinks tr:nth-child(odd) {
|
||||||
background-color: var(--bg-2);
|
background: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#editlinks tr th,
|
#editlinks tr th,
|
||||||
|
|
@ -422,11 +416,6 @@ dialog div.dialog-actions {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
#editlinks tr .grabber img {
|
|
||||||
filter: invert();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#editlinks tr .link-name {
|
#editlinks tr .link-name {
|
||||||
width: 8em;
|
width: 8em;
|
||||||
}
|
}
|
||||||
|
|
@ -465,7 +454,6 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track {
|
#edittracks .track {
|
||||||
background-color: var(--bg-2);
|
|
||||||
transition: transform .2s ease-out, opacity .2s;
|
transition: transform .2s ease-out, opacity .2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -488,7 +476,7 @@ dialog div.dialog-actions {
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track:nth-child(even) {
|
#edittracks .track:nth-child(even) {
|
||||||
background-color: var(--bg-1);
|
background: #f0f0f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#edittracks .track-number {
|
#edittracks .track-number {
|
||||||
|
|
@ -504,6 +492,7 @@ dialog div.dialog-actions {
|
||||||
#addtrack ul {
|
#addtrack ul {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
background: #f8f8f8;
|
||||||
}
|
}
|
||||||
|
|
||||||
#addtrack ul li.new-track {
|
#addtrack ul li.new-track {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
gap: 1.2em;
|
gap: 1.2em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -45,13 +45,25 @@
|
||||||
font-weight: inherit;
|
font-weight: inherit;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
outline: none;
|
outline: none;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-info input[type="text"]:hover,
|
||||||
|
.track-info textarea:hover {
|
||||||
|
border-color: #80808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-info input[type="text"]:active,
|
||||||
|
.track-info textarea:active,
|
||||||
|
.track-info input[type="text"]:focus,
|
||||||
|
.track-info textarea:focus {
|
||||||
|
border-color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
.track-actions {
|
.track-actions {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
74
admin/static/index.js
Normal file
74
admin/static/index.js
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
const newReleaseBtn = document.getElementById("create-release");
|
||||||
|
const newArtistBtn = document.getElementById("create-artist");
|
||||||
|
const newTrackBtn = document.getElementById("create-track");
|
||||||
|
|
||||||
|
newReleaseBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = prompt("Enter an ID for this release:");
|
||||||
|
if (id == null || id == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/music", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
}).then(res => {
|
||||||
|
if (res.ok) location = "/admin/releases/" + id;
|
||||||
|
else {
|
||||||
|
res.text().then(err => {
|
||||||
|
alert("Request failed: " + err);
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create release. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newArtistBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const id = prompt("Enter an ID for this artist:");
|
||||||
|
if (id == null || id == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/artist", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({id})
|
||||||
|
}).then(res => {
|
||||||
|
res.text().then(text => {
|
||||||
|
if (res.ok) {
|
||||||
|
location = "/admin/artists/" + id;
|
||||||
|
} else {
|
||||||
|
alert("Request failed: " + text);
|
||||||
|
console.error(text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create artist. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
newTrackBtn.addEventListener("click", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const title = prompt("Enter an title for this track:");
|
||||||
|
if (title == null || title == "") return;
|
||||||
|
|
||||||
|
fetch("/api/v1/track", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({title})
|
||||||
|
}).then(res => {
|
||||||
|
res.text().then(text => {
|
||||||
|
if (res.ok) {
|
||||||
|
location = "/admin/tracks/" + text;
|
||||||
|
} else {
|
||||||
|
alert("Request failed: " + text);
|
||||||
|
console.error(text);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}).catch(err => {
|
||||||
|
alert("Failed to create track. Check the console for details.");
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -8,7 +8,7 @@ form#search-form {
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: var(--fg-0);
|
color: var(--fg-0);
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -23,7 +23,7 @@ div#search {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
color: var(--fg-1);
|
color: var(--fg-1);
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,8 +100,8 @@ td.log-content {
|
||||||
|
|
||||||
#logs .log.warn {
|
#logs .log.warn {
|
||||||
color: var(--col-on-warn);
|
color: var(--col-on-warn);
|
||||||
background-color: var(--col-warn);
|
background: var(--col-warn);
|
||||||
}
|
}
|
||||||
#logs .log.warn:hover {
|
#logs .log.warn:hover {
|
||||||
background-color: var(--col-warn-hover);
|
background: var(--col-warn-hover);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
gap: 1em;
|
gap: 1em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -67,14 +67,14 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: var(--bg-3);
|
background: var(--bg-3);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: var(--shadow-sm);
|
||||||
|
|
||||||
transition: color .1s ease-out, background .1s ease-out;
|
transition: color .1s ease-out, background .1s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.release .release-actions a:hover {
|
.release .release-actions a:hover {
|
||||||
background-color: var(--bg-0);
|
background: var(--bg-0);
|
||||||
color: var(--fg-3);
|
color: var(--fg-3);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
document.addEventListener('readystatechange', () => {
|
|
||||||
const newReleaseBtn = document.getElementById("create-release");
|
|
||||||
if (newReleaseBtn) newReleaseBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const id = prompt("Enter an ID for this release:");
|
|
||||||
if (id == null || id == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/music", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({id})
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ok) location = "/admin/releases/" + id;
|
|
||||||
else {
|
|
||||||
res.text().then(err => {
|
|
||||||
alert(err);
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create release. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
gap: .5em;
|
gap: .5em;
|
||||||
|
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
background-color: var(--bg-2);
|
background: var(--bg-2);
|
||||||
box-shadow: var(--shadow-md);
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
transition: background .1s ease-out, color .1s ease-out;
|
transition: background .1s ease-out, color .1s ease-out;
|
||||||
|
|
@ -44,6 +44,11 @@
|
||||||
opacity: .5;
|
opacity: .5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tracks .track-album.empty {
|
||||||
|
color: #ff2020;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#tracks .track-description {
|
#tracks .track-description {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
@ -62,4 +67,61 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
/*
|
||||||
|
justify-content: space-between;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
.track {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
padding: 1em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: .5em;
|
||||||
|
|
||||||
|
border-radius: 8px;
|
||||||
|
background-color: var(--bg-2);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
|
||||||
|
transition: color .1s ease-out, background-color .1s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-id {
|
||||||
|
width: fit-content;
|
||||||
|
font-family: "Monaspace Argon", monospace;
|
||||||
|
font-size: .8em;
|
||||||
|
font-style: italic;
|
||||||
|
line-height: 1em;
|
||||||
|
user-select: all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-album {
|
||||||
|
margin-left: auto;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: .75em;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-album.empty {
|
||||||
|
color: #ff2020;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-description {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-lyrics {
|
||||||
|
max-height: 10em;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track .empty {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
const newTrackBtn = document.getElementById("create-track");
|
|
||||||
if (newTrackBtn) newTrackBtn.addEventListener("click", event => {
|
|
||||||
event.preventDefault();
|
|
||||||
const title = prompt("Enter an title for this track:");
|
|
||||||
if (title == null || title == "") return;
|
|
||||||
|
|
||||||
fetch("/api/v1/track", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({title})
|
|
||||||
}).then(res => {
|
|
||||||
res.text().then(text => {
|
|
||||||
if (res.ok) {
|
|
||||||
location = "/admin/tracks/" + text;
|
|
||||||
} else {
|
|
||||||
alert(text);
|
|
||||||
console.error(text);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch(err => {
|
|
||||||
alert("Failed to create track. Check the console for details.");
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -12,12 +12,12 @@
|
||||||
|
|
||||||
<form action="/api/v1/music/{{.Release.ID}}/tracks">
|
<form action="/api/v1/music/{{.Release.ID}}/tracks">
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Release.Tracks}}
|
{{range $i, $track := .Release.Tracks}}
|
||||||
<li class="track" data-track="{{.ID}}" data-title="{{.Title}}" data-number="{{.Number}}" draggable="true">
|
<li class="track" data-track="{{$track.ID}}" data-title="{{$track.Title}}" data-number="{{$track.Add $i 1}}" draggable="true">
|
||||||
<div>
|
<div>
|
||||||
<p class="track-name">
|
<p class="track-name">
|
||||||
<span class="track-number">{{.Number}}</span>
|
<span class="track-number">{{.Add $i 1}}</span>
|
||||||
{{.Title}}
|
{{$track.Title}}
|
||||||
</p>
|
</p>
|
||||||
<a class="delete">Delete</a>
|
<a class="delete">Delete</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -49,6 +49,7 @@
|
||||||
|
|
||||||
deleteBtn.addEventListener("click", e => {
|
deleteBtn.addEventListener("click", e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!confirm("Are you sure you want to remove " + trackTitle + "?")) return;
|
||||||
trackItem.remove();
|
trackItem.remove();
|
||||||
refreshTrackNumbers();
|
refreshTrackNumbers();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="credits" class="card">
|
<div class="card" id="credits">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>Credits <small>({{len .Release.Credits}} total)</small></h2>
|
<h2>Credits <small>({{len .Release.Credits}} total)</small></h2>
|
||||||
<a class="button edit"
|
<a class="button edit"
|
||||||
|
|
@ -110,7 +110,6 @@
|
||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{range .Release.Credits}}
|
{{range .Release.Credits}}
|
||||||
<div class="credit">
|
<div class="credit">
|
||||||
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
<img src="{{.Artist.GetAvatar}}" alt="" width="64" loading="lazy" class="artist-avatar">
|
||||||
|
|
@ -126,13 +125,13 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .Release.Credits}}
|
{{if not .Release.Credits}}
|
||||||
<p>This release has no credits.</p>
|
<p>There are no credits.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="links" class="card">
|
<div class="card" id="links">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h2>Links <small>({{len .Release.Links}} total)</small></h2>
|
<h2>Links ({{len .Release.Links}})</h2>
|
||||||
<a class="button edit"
|
<a class="button edit"
|
||||||
href="/admin/releases/{{.Release.ID}}/editlinks"
|
href="/admin/releases/{{.Release.ID}}/editlinks"
|
||||||
hx-get="/admin/releases/{{.Release.ID}}/editlinks"
|
hx-get="/admin/releases/{{.Release.ID}}/editlinks"
|
||||||
|
|
@ -140,21 +139,16 @@
|
||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{if .Release.Links}}
|
|
||||||
<ul>
|
<ul>
|
||||||
{{range .Release.Links}}
|
{{range .Release.Links}}
|
||||||
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a>
|
<a href="{{.URL}}" target="_blank" class="button" data-name="{{.Name}}">{{.Name}} <img class="icon" src="/img/external-link.svg"/></a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{else}}
|
|
||||||
<p>This release has no links.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tracks" class="card">
|
<div class="card" id="tracks">
|
||||||
<div class="card-header">
|
<div class="card-header" id="tracks">
|
||||||
<h2>Tracks <small>({{len .Release.Tracks}} total)</small></h2>
|
<h2>Tracklist ({{len .Release.Tracks}})</h2>
|
||||||
<a class="button edit"
|
<a class="button edit"
|
||||||
href="/admin/releases/{{.Release.ID}}/edittracks"
|
href="/admin/releases/{{.Release.ID}}/edittracks"
|
||||||
hx-get="/admin/releases/{{.Release.ID}}/edittracks"
|
hx-get="/admin/releases/{{.Release.ID}}/edittracks"
|
||||||
|
|
@ -162,13 +156,9 @@
|
||||||
hx-swap="beforeend"
|
hx-swap="beforeend"
|
||||||
>Edit</a>
|
>Edit</a>
|
||||||
</div>
|
</div>
|
||||||
|
{{range $i, $track := .Release.Tracks}}
|
||||||
{{range .Release.Tracks}}
|
|
||||||
{{block "track" .}}{{end}}
|
{{block "track" .}}{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if not .Release.Tracks}}
|
|
||||||
<p>This release has no tracks.</p>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card" id="danger">
|
<div class="card" id="danger">
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/admin/static/releases.js"></script>
|
|
||||||
<script type="module" src="/admin/static/artists.js"></script>
|
<script type="module" src="/admin/static/artists.js"></script>
|
||||||
<script type="module" src="/admin/static/tracks.js"></script>
|
<script type="module" src="/admin/static/index.js"></script>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,4 @@
|
||||||
<p>There are no releases.</p>
|
<p>There are no releases.</p>
|
||||||
{{end}}
|
{{end}}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script type="module" src="/admin/static/releases.js"></script>
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,22 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div id="tracks">
|
<div id="tracks">
|
||||||
{{range .Tracks}}
|
{{range $Track := .Tracks}}
|
||||||
{{block "track" .}}{{end}}
|
<div class="track">
|
||||||
|
<h2 class="track-title">
|
||||||
|
<a href="/admin/tracks/{{$Track.ID}}">{{$Track.Title}}</a>
|
||||||
|
</h2>
|
||||||
|
{{if $Track.Description}}
|
||||||
|
<p class="track-description">{{$Track.GetDescriptionHTML}}</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="track-description empty">No description provided.</p>
|
||||||
|
{{end}}
|
||||||
|
{{if $Track.Lyrics}}
|
||||||
|
<p class="track-lyrics">{{$Track.GetLyricsHTML}}</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="track-lyrics empty">There are no lyrics.</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
package templates
|
package templates
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model"
|
"arimelody-web/log"
|
||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
|
|
@ -163,11 +163,11 @@ var NewTrackTemplate = template.Must(template.Must(BaseTemplate.Clone()).Parse(c
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
func parseLevel(level model.LogLevel) string {
|
func parseLevel(level log.LogLevel) string {
|
||||||
switch level {
|
switch level {
|
||||||
case model.LEVEL_INFO:
|
case log.LEVEL_INFO:
|
||||||
return "INFO"
|
return "INFO"
|
||||||
case model.LEVEL_WARN:
|
case log.LEVEL_WARN:
|
||||||
return "WARN"
|
return "WARN"
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%d?", level)
|
return fmt.Sprintf("%d?", level)
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/admin/templates"
|
"arimelody-web/admin/templates"
|
||||||
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func serveTracks(app *app.AppState) http.Handler {
|
func serveTracks(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
|
@ -22,7 +22,7 @@ func serveTracks(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
tracks, err := app.MusicService.GetAllTracks()
|
tracks, err := controller.GetAllTracks(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch tracks: %s\n", err)
|
fmt.Printf("WARN: Failed to fetch tracks: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -45,11 +45,11 @@ func serveTracks(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveTrack(app *app.AppState, trackID string) http.Handler {
|
func serveTrack(app *model.AppState, trackID string) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
track, err := app.MusicService.GetTrackByID(trackID)
|
track, err := controller.GetTrack(app.DB, trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err)
|
fmt.Printf("WARN: Failed to serve admin track page for %s: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -60,7 +60,7 @@ func serveTrack(app *app.AppState, trackID string) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
releases, err := app.MusicService.GetTrackReleases(trackID)
|
releases, err := controller.GetTrackReleases(app.DB, track.ID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to fetch releases for track %s: %s\n", trackID, err)
|
fmt.Printf("WARN: Failed to fetch releases for track %s: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
|
||||||
39
api/api.go
39
api/api.go
|
|
@ -1,19 +1,17 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"arimelody-web/controller"
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
|
||||||
"arimelody-web/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Handler(app *app.AppState) http.Handler {
|
func Handler(app *model.AppState) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
// TODO: generate API keys on the frontend
|
// TODO: generate API keys on the frontend
|
||||||
|
|
@ -22,9 +20,9 @@ func Handler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/v1/artist/", http.StripPrefix("/v1/artist", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var artistID = strings.Split(r.URL.Path[1:], "/")[0]
|
var artistID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||||
artist, err := app.MusicService.GetArtistByID(artistID)
|
artist, err := controller.GetArtist(app.DB, artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsNotExistError(err) {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -64,9 +62,9 @@ func Handler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/v1/music/", http.StripPrefix("/v1/music", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var releaseID = strings.Split(r.URL.Path[1:], "/")[0]
|
var releaseID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||||
release, err := app.MusicService.GetFullReleaseByID(releaseID)
|
release, err := controller.GetRelease(app.DB, releaseID, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsNotExistError(err) {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -106,9 +104,9 @@ func Handler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/v1/track/", http.StripPrefix("/v1/track", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var trackID = strings.Split(r.URL.Path[1:], "/")[0]
|
var trackID = strings.Split(r.URL.Path[1:], "/")[0]
|
||||||
track, err := app.MusicService.GetTrackByID(trackID)
|
track, err := controller.GetTrack(app.DB, trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsNotExistError(err) {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +166,7 @@ func requireAccount(next http.Handler) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSession(app *app.AppState, r *http.Request) (*model.Session, error) {
|
func getSession(app *model.AppState, r *http.Request) (*model.Session, error) {
|
||||||
var token string
|
var token string
|
||||||
|
|
||||||
// check cookies first
|
// check cookies first
|
||||||
|
|
@ -186,12 +184,9 @@ func getSession(app *app.AppState, r *http.Request) (*model.Session, error) {
|
||||||
if token == "" { return nil, nil }
|
if token == "" { return nil, nil }
|
||||||
|
|
||||||
// fetch existing session
|
// fetch existing session
|
||||||
session, err := controller.GetSession(app, token)
|
session, err := controller.GetSession(app.DB, token)
|
||||||
|
|
||||||
if errors.IsValidationError(err) {
|
if err != nil && !strings.Contains(err.Error(), "no rows") {
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
return nil, fmt.Errorf("Failed to retrieve session: %v\n", err)
|
return nil, fmt.Errorf("Failed to retrieve session: %v\n", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody-web/model"
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/log"
|
||||||
"arimelody-web/errors"
|
"arimelody-web/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ServeAllArtists(app *app.AppState) http.Handler {
|
func ServeAllArtists(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var artists = []*model.Artist{}
|
var artists = []*model.Artist{}
|
||||||
artists, err := app.MusicService.GetAllArtists()
|
artists, err := controller.GetAllArtists(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve all artists: %s\n", err)
|
fmt.Printf("WARN: Failed to serve all artists: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -35,7 +35,7 @@ func ServeAllArtists(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeArtist(app *app.AppState, artist *model.Artist) http.Handler {
|
func ServeArtist(app *model.AppState, artist *model.Artist) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
type (
|
type (
|
||||||
creditJSON struct {
|
creditJSON struct {
|
||||||
|
|
@ -53,9 +53,9 @@ func ServeArtist(app *app.AppState, artist *model.Artist) http.Handler {
|
||||||
)
|
)
|
||||||
|
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
showHiddenReleases := session != nil && session.Account != nil
|
show_hidden_releases := session != nil && session.Account != nil
|
||||||
|
|
||||||
dbCredits, err := app.MusicService.GetArtistCredits(artist.ID, showHiddenReleases)
|
dbCredits, err := controller.GetArtistCredits(app.DB, artist.ID, show_hidden_releases)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err)
|
fmt.Printf("WARN: Failed to retrieve artist credits for %s: %v\n", artist.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -87,44 +87,41 @@ func ServeArtist(app *app.AppState, artist *model.Artist) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateArtist(app *app.AppState) http.Handler {
|
func CreateArtist(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
type CreateArtistDTO struct {
|
var artist model.Artist
|
||||||
ID string `json:"id"`
|
err := json.NewDecoder(r.Body).Decode(&artist)
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
dto := &CreateArtistDTO{}
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(dto)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.MusicService.CreateArtist(dto.ID, dto.Name, "", "")
|
if artist.ID == "" {
|
||||||
|
http.Error(w, "Artist ID cannot be blank\n", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if artist.Name == "" { artist.Name = artist.ID }
|
||||||
|
|
||||||
|
err = controller.CreateArtist(app.DB, &artist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
http.Error(w, fmt.Sprintf("Artist %s already exists\n", dto.ID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Artist %s already exists\n", artist.ID), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("WARN: Failed to create artist %s: %s\n", dto.ID, err)
|
fmt.Printf("WARN: Failed to create artist %s: %s\n", artist.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" created by \"%s\".", dto.Name, session.Account.Username)
|
app.Log.Info(log.TYPE_ARTIST, "Artist \"%s\" created by \"%s\".", artist.Name, session.Account.Username)
|
||||||
|
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateArtist(app *app.AppState, artist *model.Artist) http.Handler {
|
func UpdateArtist(app *model.AppState, artist *model.Artist) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
|
@ -159,13 +156,9 @@ func UpdateArtist(app *app.AppState, artist *model.Artist) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.MusicService.UpdateArtist(artist)
|
err = controller.UpdateArtist(app.DB, artist)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -173,21 +166,17 @@ func UpdateArtist(app *app.AppState, artist *model.Artist) http.Handler {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" updated by \"%s\".", artist.Name, session.Account.Username)
|
app.Log.Info(log.TYPE_ARTIST, "Artist \"%s\" updated by \"%s\".", artist.Name, session.Account.Username)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteArtist(app *app.AppState, artist *model.Artist) http.Handler {
|
func DeleteArtist(app *model.AppState, artist *model.Artist) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
err := app.MusicService.DeleteArtist(artist.ID)
|
err := controller.DeleteArtist(app.DB, artist.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +184,6 @@ func DeleteArtist(app *app.AppState, artist *model.Artist) http.Handler {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ARTIST, "Artist \"%s\" deleted by \"%s\".", artist.Name, session.Account.Username)
|
app.Log.Info(log.TYPE_ARTIST, "Artist \"%s\" deleted by \"%s\".", artist.Name, session.Account.Username)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
243
api/release.go
243
api/release.go
|
|
@ -1,22 +1,21 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody-web/controller"
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model"
|
"arimelody-web/log"
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
"arimelody-web/errors"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
|
func ServeRelease(app *model.AppState, release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// only allow authorised users to view hidden releases
|
// only allow authorised users to view hidden releases
|
||||||
privileged := false
|
privileged := false
|
||||||
|
|
@ -40,50 +39,50 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
type (
|
type (
|
||||||
TrackDTO struct {
|
Track struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Lyrics string `json:"lyrics"`
|
Lyrics string `json:"lyrics"`
|
||||||
}
|
}
|
||||||
|
|
||||||
CreditDTO struct {
|
Credit struct {
|
||||||
*model.Artist
|
*model.Artist
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Primary bool `json:"primary"`
|
Primary bool `json:"primary"`
|
||||||
}
|
}
|
||||||
|
|
||||||
ReleaseDTO struct {
|
Release struct {
|
||||||
*model.Release
|
*model.Release
|
||||||
Tracks []TrackDTO `json:"tracks"`
|
Tracks []Track `json:"tracks"`
|
||||||
Credits []CreditDTO `json:"credits"`
|
Credits []Credit `json:"credits"`
|
||||||
Links map[string]string `json:"links"`
|
Links map[string]string `json:"links"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
response := ReleaseDTO{
|
response := Release{
|
||||||
Release: release,
|
Release: release,
|
||||||
Tracks: []TrackDTO{},
|
Tracks: []Track{},
|
||||||
Credits: []CreditDTO{},
|
Credits: []Credit{},
|
||||||
Links: make(map[string]string),
|
Links: make(map[string]string),
|
||||||
}
|
}
|
||||||
|
|
||||||
if release.IsReleased() || privileged {
|
if release.IsReleased() || privileged {
|
||||||
// get credits
|
// get credits
|
||||||
credits, err := app.MusicService.GetReleaseCredits(release.ID)
|
credits, err := controller.GetReleaseCredits(app.DB, release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to serve release %s: Credits: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, credit := range credits {
|
for _, credit := range credits {
|
||||||
artist, err := app.MusicService.GetArtistByID(credit.Artist.ID)
|
artist, err := controller.GetArtist(app.DB, credit.Artist.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to serve release %s: Artists: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Credits = append(response.Credits, CreditDTO{
|
response.Credits = append(response.Credits, Credit{
|
||||||
Artist: artist,
|
Artist: artist,
|
||||||
Role: credit.Role,
|
Role: credit.Role,
|
||||||
Primary: credit.Primary,
|
Primary: credit.Primary,
|
||||||
|
|
@ -91,14 +90,14 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get tracks
|
// get tracks
|
||||||
tracks, err := app.MusicService.GetReleaseTracks(release.ID)
|
tracks, err := controller.GetReleaseTracks(app.DB, release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to serve release %s: Tracks: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, track := range tracks {
|
for _, track := range tracks {
|
||||||
response.Tracks = append(response.Tracks, TrackDTO{
|
response.Tracks = append(response.Tracks, Track{
|
||||||
Title: track.Title,
|
Title: track.Title,
|
||||||
Description: track.Description,
|
Description: track.Description,
|
||||||
Lyrics: track.Lyrics,
|
Lyrics: track.Lyrics,
|
||||||
|
|
@ -106,7 +105,7 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// get links
|
// get links
|
||||||
links, err := app.MusicService.GetReleaseLinks(release.ID)
|
links, err := controller.GetReleaseLinks(app.DB, release.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to serve release %s: Links: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -128,9 +127,9 @@ func ServeRelease(app *app.AppState, release *model.Release) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeCatalog(app *app.AppState) http.Handler {
|
func ServeCatalog(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
releases, err := app.MusicService.GetAllFullReleases(false, 0)
|
releases, err := controller.GetAllReleases(app.DB, false, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
@ -189,66 +188,57 @@ func ServeCatalog(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateRelease(app *app.AppState) http.Handler {
|
func CreateRelease(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
type CreateReleaseDTO struct {
|
var release model.Release
|
||||||
ID string `json:"id"`
|
err := json.NewDecoder(r.Body).Decode(&release)
|
||||||
Title string `json:"title"`
|
|
||||||
ReleaseType string `json:"type"`
|
|
||||||
ReleaseDate time.Time `json:"release_date"`
|
|
||||||
Artwork string `json:"artwork"`
|
|
||||||
}
|
|
||||||
var dto CreateReleaseDTO
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&dto)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if dto.ReleaseType == "" { dto.ReleaseType = string(model.Single) }
|
if release.ID == "" {
|
||||||
if dto.ReleaseDate != time.Unix(0, 0) {
|
http.Error(w, "Release ID cannot be empty\n", http.StatusBadRequest)
|
||||||
dto.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
|
return
|
||||||
}
|
}
|
||||||
if dto.Artwork == "" { dto.Artwork = model.DEFAULT_RELEASE_ARTWORK_URL }
|
|
||||||
|
|
||||||
err = app.MusicService.CreateRelease(
|
if release.Title == "" { release.Title = release.ID }
|
||||||
dto.ID,
|
if release.ReleaseType == "" { release.ReleaseType = model.Single }
|
||||||
dto.Title,
|
|
||||||
dto.ReleaseType,
|
if release.ReleaseDate != time.Unix(0, 0) {
|
||||||
dto.ReleaseDate,
|
release.ReleaseDate = time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.UTC)
|
||||||
dto.Artwork,
|
}
|
||||||
)
|
|
||||||
|
if release.Artwork == "" { release.Artwork = "/img/default-cover-art.png" }
|
||||||
|
|
||||||
|
err = controller.CreateRelease(app.DB, &release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
http.Error(w, fmt.Sprintf("Release %s already exists\n", dto.ID), http.StatusBadRequest)
|
http.Error(w, fmt.Sprintf("Release %s already exists\n", release.ID), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("WARN: Failed to create release %s: %s\n", dto.ID, err)
|
fmt.Printf("WARN: Failed to create release %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" created by \"%s\".", dto.ID, session.Account.Username)
|
app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" created by \"%s\".", release.ID, session.Account.Username)
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
encoder := json.NewEncoder(w)
|
encoder := json.NewEncoder(w)
|
||||||
encoder.SetIndent("", "\t")
|
encoder.SetIndent("", "\t")
|
||||||
err = encoder.Encode(dto)
|
err = encoder.Encode(release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", dto.ID, err)
|
fmt.Printf("WARN: Release %s created, but failed to send JSON response: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateRelease(app *app.AppState, release *model.Release) http.Handler {
|
func UpdateRelease(app *model.AppState, release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
|
@ -307,13 +297,9 @@ func UpdateRelease(app *app.AppState, release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.MusicService.UpdateRelease(release)
|
err = controller.UpdateRelease(app.DB, release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -321,63 +307,55 @@ func UpdateRelease(app *app.AppState, release *model.Release) http.Handler {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
|
app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReleaseTracks(app *app.AppState, release *model.Release) http.Handler {
|
func UpdateReleaseTracks(app *model.AppState, release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
var newTrackIDs = []string{}
|
var trackIDs = []string{}
|
||||||
err := json.NewDecoder(r.Body).Decode(&newTrackIDs)
|
err := json.NewDecoder(r.Body).Decode(&trackIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.MusicService.UpdateReleaseTracks(release.ID, newTrackIDs)
|
err = controller.UpdateReleaseTracks(app.DB, release.ID, trackIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
|
||||||
http.Error(w, "Release cannot have duplicate tracks", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to update tracks for %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" tracklist updated by \"%s\".", release.ID, session.Account.Username)
|
app.Log.Info(log.TYPE_MUSIC, "Tracklist for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateReleaseCredits(app *app.AppState, release *model.Release) http.Handler {
|
func UpdateReleaseCredits(app *model.AppState, release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
type CreditDTO struct {
|
type creditJSON struct {
|
||||||
Artist string
|
Artist string
|
||||||
Role string
|
Role string
|
||||||
Primary bool
|
Primary bool
|
||||||
}
|
}
|
||||||
var dto []CreditDTO
|
var data []creditJSON
|
||||||
err := json.NewDecoder(r.Body).Decode(&dto)
|
err := json.NewDecoder(r.Body).Decode(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var newCredits []*model.Credit
|
var credits []*model.Credit
|
||||||
for _, credit := range dto {
|
for _, credit := range data {
|
||||||
newCredits = append(newCredits, &model.Credit{
|
credits = append(credits, &model.Credit{
|
||||||
Artist: &model.Artist{
|
Artist: model.Artist{
|
||||||
ID: credit.Artist,
|
ID: credit.Artist,
|
||||||
},
|
},
|
||||||
Role: credit.Role,
|
Role: credit.Role,
|
||||||
|
|
@ -385,81 +363,56 @@ func UpdateReleaseCredits(app *app.AppState, release *model.Release) http.Handle
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.MusicService.UpdateReleaseCredits(release.ID, newCredits)
|
err = controller.UpdateReleaseCredits(app.DB, release.ID, credits)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
if strings.Contains(err.Error(), "duplicate key") {
|
||||||
http.Error(w, "Artists may only be credited once", http.StatusBadRequest)
|
http.Error(w, "Artists may only be credited once\n", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
fmt.Printf("WARN: Failed to update credits for %s: %s\n", release.ID, err)
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
|
||||||
}
|
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" credits updated by \"%s\".", release.ID, session.Account.Username)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func UpdateReleaseLinks(app *app.AppState, release *model.Release) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
session := r.Context().Value("session").(*model.Session)
|
|
||||||
|
|
||||||
type LinkDTO struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
var dto = []LinkDTO{}
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&dto)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
links := []*model.Link{}
|
|
||||||
for _, link := range dto {
|
|
||||||
links = append(links, &model.Link{ Name: link.Name, URL: link.URL })
|
|
||||||
}
|
|
||||||
|
|
||||||
err = app.MusicService.UpdateReleaseLinks(release.ID, links)
|
|
||||||
if err != nil {
|
|
||||||
if errors.IsValidationError(err) {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if strings.Contains(err.Error(), "duplicate key") {
|
|
||||||
http.Error(w, "Release cannot have duplicate link names", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
|
fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" links updated by \"%s\".", release.ID, session.Account.Username)
|
app.Log.Info(log.TYPE_MUSIC, "Credits for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteRelease(app *app.AppState, release *model.Release) http.Handler {
|
func UpdateReleaseLinks(app *model.AppState, release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
err := app.MusicService.DeleteRelease(release.ID)
|
var links = []*model.Link{}
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&links)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = controller.UpdateReleaseLinks(app.DB, release.ID, links)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errors.IsNotExistError(err) {
|
fmt.Printf("WARN: Failed to update links for %s: %s\n", release.ID, err)
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Log.Info(log.TYPE_MUSIC, "Links for release \"%s\" updated by \"%s\".", release.ID, session.Account.Username)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteRelease(app *model.AppState, release *model.Release) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
|
err := controller.DeleteRelease(app.DB, release.ID)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -467,6 +420,6 @@ func DeleteRelease(app *app.AppState, release *model.Release) http.Handler {
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username)
|
app.Log.Info(log.TYPE_MUSIC, "Release \"%s\" deleted by \"%s\".", release.ID, session.Account.Username)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
79
api/track.go
79
api/track.go
|
|
@ -1,13 +1,13 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"arimelody-web/errors"
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model"
|
"arimelody-web/log"
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
@ -17,7 +17,7 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func ServeAllTracks(app *app.AppState) http.Handler {
|
func ServeAllTracks(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
type Track struct {
|
type Track struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
|
|
@ -26,7 +26,7 @@ func ServeAllTracks(app *app.AppState) http.Handler {
|
||||||
var tracks = []Track{}
|
var tracks = []Track{}
|
||||||
|
|
||||||
var dbTracks = []*model.Track{}
|
var dbTracks = []*model.Track{}
|
||||||
dbTracks, err := app.MusicService.GetAllTracks()
|
dbTracks, err := controller.GetAllTracks(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err)
|
fmt.Printf("WARN: Failed to pull tracks from DB: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -50,9 +50,9 @@ func ServeAllTracks(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeTrack(app *app.AppState, track *model.Track) http.Handler {
|
func ServeTrack(app *model.AppState, track *model.Track) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
dbReleases, err := app.MusicService.GetTrackReleases(track.ID)
|
dbReleases, err := controller.GetTrackReleases(app.DB, track.ID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
|
fmt.Printf("WARN: Failed to pull track releases for %s from DB: %s\n", track.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -74,34 +74,30 @@ func ServeTrack(app *app.AppState, track *model.Track) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateTrack(app *app.AppState) http.Handler {
|
func CreateTrack(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
type CreateTrackDTO struct {
|
var track model.Track
|
||||||
Title string `json:"title"`
|
err := json.NewDecoder(r.Body).Decode(&track)
|
||||||
Description string `json:"description"`
|
|
||||||
Lyrics string `json:"lyrics"`
|
|
||||||
}
|
|
||||||
var dto CreateTrackDTO
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&dto)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := app.MusicService.CreateTrack(dto.Title, dto.Description, dto.Lyrics, "")
|
if track.Title == "" {
|
||||||
|
http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := controller.CreateTrack(app.DB, &track)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("WARN: Failed to create track: %s\n", err)
|
fmt.Printf("WARN: Failed to create track: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Track \"%s\" (%s) created by \"%s\".", dto.Title, id, session.Account.Username)
|
app.Log.Info(log.TYPE_MUSIC, "Track \"%s\" (%s) created by \"%s\".", track.Title, track.ID, session.Account.Username)
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "text/plain")
|
w.Header().Add("Content-Type", "text/plain")
|
||||||
w.WriteHeader(http.StatusCreated)
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
|
@ -109,7 +105,7 @@ func CreateTrack(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func UpdateTrack(app *app.AppState, track *model.Track) http.Handler {
|
func UpdateTrack(app *model.AppState, track *model.Track) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -118,34 +114,25 @@ func UpdateTrack(app *app.AppState, track *model.Track) http.Handler {
|
||||||
|
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
type UpdateTrackDTO struct {
|
err := json.NewDecoder(r.Body).Decode(&track)
|
||||||
Title string `json:"title"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
Lyrics string `json:"lyrics"`
|
|
||||||
}
|
|
||||||
var dto UpdateTrackDTO
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&dto)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
track.Title = dto.Title
|
if track.Title == "" {
|
||||||
track.Description = dto.Description
|
http.Error(w, "Track title cannot be empty\n", http.StatusBadRequest)
|
||||||
track.Lyrics = dto.Lyrics
|
return
|
||||||
|
}
|
||||||
|
|
||||||
err = app.MusicService.UpdateTrack(track)
|
err = controller.UpdateTrack(app.DB, track)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsValidationError(err) {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("WARN: Failed to update track %s: %s\n", track.ID, err)
|
fmt.Printf("WARN: Failed to update track %s: %s\n", track.ID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Track \"%s\" (%s) updated by \"%s\".", track.Title, track.ID, session.Account.Username)
|
app.Log.Info(log.TYPE_MUSIC, "Track \"%s\" (%s) updated by \"%s\".", track.Title, track.ID, session.Account.Username)
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
encoder := json.NewEncoder(w)
|
encoder := json.NewEncoder(w)
|
||||||
|
|
@ -157,7 +144,7 @@ func UpdateTrack(app *app.AppState, track *model.Track) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteTrack(app *app.AppState, track *model.Track) http.Handler {
|
func DeleteTrack(app *model.AppState, track *model.Track) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.URL.Path == "/" {
|
if r.URL.Path == "/" {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
|
|
@ -167,16 +154,12 @@ func DeleteTrack(app *app.AppState, track *model.Track) http.Handler {
|
||||||
session := r.Context().Value("session").(*model.Session)
|
session := r.Context().Value("session").(*model.Session)
|
||||||
|
|
||||||
var trackID = r.URL.Path[1:]
|
var trackID = r.URL.Path[1:]
|
||||||
err := app.MusicService.DeleteTrack(trackID)
|
err := controller.DeleteTrack(app.DB, trackID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fmt.Printf("WARN: Failed to delete track %s: %s\n", trackID, err)
|
fmt.Printf("WARN: Failed to delete track %s: %s\n", trackID, err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_MUSIC, "Track \"%s\" (%s) deleted by \"%s\".", track.Title, track.ID, session.Account.Username)
|
app.Log.Info(log.TYPE_MUSIC, "Track \"%s\" (%s) deleted by \"%s\".", track.Title, track.ID, session.Account.Username)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model"
|
"arimelody-web/log"
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
"bufio"
|
"bufio"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func HandleImageUpload(app *app.AppState, data *string, directory string, filename string) (string, error) {
|
func HandleImageUpload(app *model.AppState, data *string, directory string, filename string) (string, error) {
|
||||||
split := strings.Split(*data, ";base64,")
|
split := strings.Split(*data, ";base64,")
|
||||||
header := split[0]
|
header := split[0]
|
||||||
imageData, err := base64.StdEncoding.DecodeString(split[1])
|
imageData, err := base64.StdEncoding.DecodeString(split[1])
|
||||||
|
|
@ -50,7 +50,7 @@ func HandleImageUpload(app *app.AppState, data *string, directory string, filena
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_FILES, "\"%s\" created.", imagePath)
|
app.Log.Info(log.TYPE_FILES, "\"%s\" created.", imagePath)
|
||||||
|
|
||||||
return filename, nil
|
return filename, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
135
controller/account.go
Normal file
135
controller/account.go
Normal file
|
|
@ -0,0 +1,135 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAllAccounts(db *sqlx.DB) ([]model.Account, error) {
|
||||||
|
var accounts = []model.Account{}
|
||||||
|
|
||||||
|
err := db.Select(&accounts, "SELECT * FROM account ORDER BY created_at ASC")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return accounts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccountByID(db *sqlx.DB, id string) (*model.Account, error) {
|
||||||
|
var account = model.Account{}
|
||||||
|
|
||||||
|
err := db.Get(&account, "SELECT * FROM account WHERE id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccountByUsername(db *sqlx.DB, username string) (*model.Account, error) {
|
||||||
|
var account = model.Account{}
|
||||||
|
|
||||||
|
err := db.Get(&account, "SELECT * FROM account WHERE username=$1", username)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccountByEmail(db *sqlx.DB, email string) (*model.Account, error) {
|
||||||
|
var account = model.Account{}
|
||||||
|
|
||||||
|
err := db.Get(&account, "SELECT * FROM account WHERE email=$1", email)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAccountBySession(db *sqlx.DB, sessionToken string) (*model.Account, error) {
|
||||||
|
if sessionToken == "" { return nil, nil }
|
||||||
|
|
||||||
|
account := model.Account{}
|
||||||
|
|
||||||
|
err := db.Get(&account, "SELECT account.* FROM account JOIN token ON id=account WHERE token=$1", sessionToken)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "no rows") {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &account, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateAccount(db *sqlx.DB, account *model.Account) error {
|
||||||
|
err := db.Get(
|
||||||
|
&account.ID,
|
||||||
|
"INSERT INTO account (username, password, email, avatar_url) " +
|
||||||
|
"VALUES ($1, $2, $3, $4) " +
|
||||||
|
"RETURNING id",
|
||||||
|
account.Username,
|
||||||
|
account.Password,
|
||||||
|
account.Email,
|
||||||
|
account.AvatarURL,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateAccount(db *sqlx.DB, account *model.Account) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"UPDATE account " +
|
||||||
|
"SET username=$2,password=$3,email=$4,avatar_url=$5 " +
|
||||||
|
"WHERE id=$1",
|
||||||
|
account.ID,
|
||||||
|
account.Username,
|
||||||
|
account.Password,
|
||||||
|
account.Email,
|
||||||
|
account.AvatarURL,
|
||||||
|
)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteAccount(db *sqlx.DB, accountID string) error {
|
||||||
|
_, err := db.Exec("DELETE FROM account WHERE id=$1", accountID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func IncrementAccountFails(db *sqlx.DB, accountID string) (bool, error) {
|
||||||
|
failAttempts := 0
|
||||||
|
err := db.Get(&failAttempts, "UPDATE account SET fail_attempts = fail_attempts + 1 WHERE id=$1 RETURNING fail_attempts", accountID)
|
||||||
|
if err != nil { return false, err }
|
||||||
|
locked := false
|
||||||
|
if failAttempts >= model.MAX_LOGIN_FAIL_ATTEMPTS {
|
||||||
|
err = LockAccount(db, accountID)
|
||||||
|
if err != nil { return false, err }
|
||||||
|
locked = true
|
||||||
|
}
|
||||||
|
return locked, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func LockAccount(db *sqlx.DB, accountID string) error {
|
||||||
|
_, err := db.Exec("UPDATE account SET locked = true WHERE id=$1", accountID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func UnlockAccount(db *sqlx.DB, accountID string) error {
|
||||||
|
_, err := db.Exec("UPDATE account SET locked = false, fail_attempts = 0 WHERE id=$1", accountID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -1,29 +1,17 @@
|
||||||
package music
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetAllArtists() ([]*model.Artist, error) {
|
// DATABASE
|
||||||
var artists = []*model.Artist{}
|
|
||||||
|
|
||||||
err := repo.db.Select(&artists, "SELECT * FROM artist")
|
func GetArtist(db *sqlx.DB, id string) (*model.Artist, error) {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return artists, nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) GetArtistCount() (int, error) {
|
|
||||||
var count int
|
|
||||||
err := repo.db.Get(&count, "SELECT count(*) FROM artist")
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetArtistByID(id string) (*model.Artist, error) {
|
|
||||||
var artist = model.Artist{}
|
var artist = model.Artist{}
|
||||||
|
|
||||||
err := repo.db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id)
|
err := db.Get(&artist, "SELECT * FROM artist WHERE id=$1", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -31,10 +19,26 @@ func (repo *MusicRepositoryPostgres) GetArtistByID(id string) (*model.Artist, er
|
||||||
return &artist, nil
|
return &artist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error) {
|
func GetAllArtists(db *sqlx.DB) ([]*model.Artist, error) {
|
||||||
var artists = []*model.Artist{}
|
var artists = []*model.Artist{}
|
||||||
|
|
||||||
err := repo.db.Select(&artists,
|
err := db.Select(&artists, "SELECT * FROM artist")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return artists, nil
|
||||||
|
}
|
||||||
|
func GetArtistCount(db *sqlx.DB) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := db.Get(&count, "SELECT count(*) FROM artist")
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetArtistsNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Artist, error) {
|
||||||
|
var artists = []*model.Artist{}
|
||||||
|
|
||||||
|
err := db.Select(&artists,
|
||||||
"SELECT * FROM artist "+
|
"SELECT * FROM artist "+
|
||||||
"WHERE id NOT IN "+
|
"WHERE id NOT IN "+
|
||||||
"(SELECT artist FROM musiccredit WHERE release=$1)",
|
"(SELECT artist FROM musiccredit WHERE release=$1)",
|
||||||
|
|
@ -46,15 +50,15 @@ func (repo *MusicRepositoryPostgres) GetArtistsNotOnRelease(releaseID string) ([
|
||||||
return artists, nil
|
return artists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error) {
|
func GetArtistCredits(db *sqlx.DB, artistID string, show_hidden bool) ([]*model.Credit, error) {
|
||||||
var query string = "SELECT release.id,title,artwork,release_date,artist.id,name,website,avatar,role,is_primary "+
|
var query string = "SELECT release.id,title,artwork,release_date,artist.id,name,website,avatar,role,is_primary "+
|
||||||
"FROM musiccredit "+
|
"FROM musiccredit "+
|
||||||
"JOIN musicrelease AS release ON release=release.id "+
|
"JOIN musicrelease AS release ON release=release.id "+
|
||||||
"JOIN artist ON artist=artist.id "+
|
"JOIN artist ON artist=artist.id "+
|
||||||
"WHERE artist=$1 "
|
"WHERE artist=$1 "
|
||||||
if !showHidden { query += "AND visible=true " }
|
if !show_hidden { query += "AND visible=true " }
|
||||||
query += "ORDER BY release_date DESC"
|
query += "ORDER BY release_date DESC"
|
||||||
rows, err := repo.db.Query(query, artistID)
|
rows, err := db.Query(query, artistID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -66,10 +70,7 @@ func (repo *MusicRepositoryPostgres) GetArtistCredits(artistID string, showHidde
|
||||||
}
|
}
|
||||||
var credits []*model.Credit
|
var credits []*model.Credit
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
credit := &model.Credit{
|
var credit model.Credit
|
||||||
Release: &model.Release{},
|
|
||||||
Artist: &model.Artist{},
|
|
||||||
}
|
|
||||||
err = rows.Scan(
|
err = rows.Scan(
|
||||||
&credit.Release.ID,
|
&credit.Release.ID,
|
||||||
&credit.Release.Title,
|
&credit.Release.Title,
|
||||||
|
|
@ -84,42 +85,44 @@ func (repo *MusicRepositoryPostgres) GetArtistCredits(artistID string, showHidde
|
||||||
)
|
)
|
||||||
|
|
||||||
otherArtists := []NamePrimary{}
|
otherArtists := []NamePrimary{}
|
||||||
err = repo.db.Select(&otherArtists,
|
err = db.Select(&otherArtists,
|
||||||
"SELECT name,is_primary FROM artist "+
|
"SELECT name,is_primary FROM artist "+
|
||||||
"JOIN musiccredit ON artist=id "+
|
"JOIN musiccredit ON artist=id "+
|
||||||
"WHERE release=$1",
|
"WHERE release=$1",
|
||||||
credit.Release.ID)
|
credit.Release.ID)
|
||||||
for _, otherCredit := range otherArtists {
|
for _, otherCredit := range otherArtists {
|
||||||
credit.Release.Credits = append(credit.Release.Credits, &model.Credit{
|
credit.Release.Credits = append(credit.Release.Credits, &model.Credit{
|
||||||
Artist: &model.Artist{
|
Artist: model.Artist{
|
||||||
Name: otherCredit.Name,
|
Name: otherCredit.Name,
|
||||||
},
|
},
|
||||||
Primary: otherCredit.Primary,
|
Primary: otherCredit.Primary,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
credits = append(credits, credit)
|
credits = append(credits, &credit)
|
||||||
}
|
}
|
||||||
|
|
||||||
return credits, nil
|
return credits, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) CreateArtist(
|
func CreateArtist(db *sqlx.DB, artist *model.Artist) error {
|
||||||
id string,
|
_, err := db.Exec(
|
||||||
name string,
|
|
||||||
website string,
|
|
||||||
avatar string,
|
|
||||||
) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"INSERT INTO artist (id, name, website, avatar) "+
|
"INSERT INTO artist (id, name, website, avatar) "+
|
||||||
"VALUES ($1, $2, $3, $4)",
|
"VALUES ($1, $2, $3, $4)",
|
||||||
id, name, website, avatar,
|
artist.ID,
|
||||||
|
artist.Name,
|
||||||
|
artist.Website,
|
||||||
|
artist.Avatar,
|
||||||
)
|
)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateArtist(artist *model.Artist) error {
|
func UpdateArtist(db *sqlx.DB, artist *model.Artist) error {
|
||||||
_, err := repo.db.Exec(
|
_, err := db.Exec(
|
||||||
"UPDATE artist "+
|
"UPDATE artist "+
|
||||||
"SET name=$2, website=$3, avatar=$4 "+
|
"SET name=$2, website=$3, avatar=$4 "+
|
||||||
"WHERE id=$1",
|
"WHERE id=$1",
|
||||||
|
|
@ -128,27 +131,22 @@ func (repo *MusicRepositoryPostgres) UpdateArtist(artist *model.Artist) error {
|
||||||
artist.Website,
|
artist.Website,
|
||||||
artist.Avatar,
|
artist.Avatar,
|
||||||
)
|
)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
func (repo *MusicRepositoryPostgres) UpdateArtistID(oldID string, newID string) error {
|
}
|
||||||
_, err := repo.db.Exec("UPDATE artist SET id=$2 WHERE id=$1", oldID, newID)
|
|
||||||
return err
|
return nil
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateArtistName(id string, name string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE artist SET name=$2 WHERE id=$1", id, name)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateArtistWebsite(id string, website string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE artist SET website=$2 WHERE id=$1", id, website)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateArtistAvatar(id string, avatar string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE artist SET avatar=$2 WHERE id=$1", id, avatar)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) DeleteArtist(id string) (string, error) {
|
func DeleteArtist(db *sqlx.DB, artistID string) error {
|
||||||
var deletedID string
|
_, err := db.Exec(
|
||||||
err := repo.db.Get(&deletedID, "DELETE FROM artist WHERE id=$1", id)
|
"DELETE FROM artist "+
|
||||||
return deletedID, err
|
"WHERE id=$1",
|
||||||
|
artistID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -1,28 +1,28 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
|
|
||||||
"github.com/pelletier/go-toml/v2"
|
"github.com/pelletier/go-toml/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetConfig() app.Config {
|
func GetConfig() model.Config {
|
||||||
configFile := os.Getenv("ARIMELODY_CONFIG")
|
configFile := os.Getenv("ARIMELODY_CONFIG")
|
||||||
if configFile == "" {
|
if configFile == "" {
|
||||||
configFile = "config.toml"
|
configFile = "config.toml"
|
||||||
}
|
}
|
||||||
|
|
||||||
config := app.Config{
|
config := model.Config{
|
||||||
BaseUrl: "https://arimelody.space",
|
BaseUrl: "https://arimelody.space",
|
||||||
Host: "0.0.0.0",
|
Host: "0.0.0.0",
|
||||||
Port: 8080,
|
Port: 8080,
|
||||||
TrustedProxies: []string{ "127.0.0.1" },
|
TrustedProxies: []string{ "127.0.0.1" },
|
||||||
DB: app.DBConfig{
|
DB: model.DBConfig{
|
||||||
Host: "127.0.0.1",
|
Host: "127.0.0.1",
|
||||||
Port: 5432,
|
Port: 5432,
|
||||||
User: "arimelody",
|
User: "arimelody",
|
||||||
|
|
@ -53,7 +53,7 @@ func GetConfig() app.Config {
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleConfigOverrides(config *app.Config) error {
|
func handleConfigOverrides(config *model.Config) error {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if env, has := os.LookupEnv("ARIMELODY_BASE_URL"); has { config.BaseUrl = env }
|
if env, has := os.LookupEnv("ARIMELODY_BASE_URL"); has { config.BaseUrl = env }
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
"net/http"
|
"net/http"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns the request's original IP address, resolving the `x-forwarded-for`
|
// Returns the request's original IP address, resolving the `x-forwarded-for`
|
||||||
// header if the request originates from a trusted proxy.
|
// header if the request originates from a trusted proxy.
|
||||||
func ResolveIP(app *app.AppState, r *http.Request) string {
|
func ResolveIP(app *model.AppState, r *http.Request) string {
|
||||||
addr := strings.Split(r.RemoteAddr, ":")[0]
|
addr := strings.Split(r.RemoteAddr, ":")[0]
|
||||||
if slices.Contains(app.Config.TrustedProxies, addr) {
|
if slices.Contains(app.Config.TrustedProxies, addr) {
|
||||||
forwardedFor := r.Header.Get("x-forwarded-for")
|
forwardedFor := r.Header.Get("x-forwarded-for")
|
||||||
|
|
|
||||||
327
controller/release.go
Normal file
327
controller/release.go
Normal file
|
|
@ -0,0 +1,327 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"arimelody-web/model"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetRelease(db *sqlx.DB, id string, full bool) (*model.Release, error) {
|
||||||
|
var release = model.Release{}
|
||||||
|
|
||||||
|
err := db.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if full {
|
||||||
|
// get credits
|
||||||
|
credits, err := GetReleaseCredits(db, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Credits: %s", err)
|
||||||
|
}
|
||||||
|
for _, credit := range credits {
|
||||||
|
release.Credits = append(release.Credits, credit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get tracks
|
||||||
|
tracks, err := GetReleaseTracks(db, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Tracks: %s", err)
|
||||||
|
}
|
||||||
|
for _, track := range tracks {
|
||||||
|
release.Tracks = append(release.Tracks, track)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get links
|
||||||
|
links, err := GetReleaseLinks(db, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Links: %s", err)
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
release.Links = append(release.Links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &release, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllReleases(db *sqlx.DB, onlyVisible bool, limit int, full bool) ([]*model.Release, error) {
|
||||||
|
var releases = []*model.Release{}
|
||||||
|
|
||||||
|
query := "SELECT * FROM musicrelease"
|
||||||
|
if onlyVisible {
|
||||||
|
query += " WHERE visible=true"
|
||||||
|
}
|
||||||
|
query += " ORDER BY release_date DESC"
|
||||||
|
var err error
|
||||||
|
if limit > 0 {
|
||||||
|
err = db.Select(&releases, query + " LIMIT $1", limit)
|
||||||
|
} else {
|
||||||
|
err = db.Select(&releases, query)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, release := range releases {
|
||||||
|
// get credits
|
||||||
|
credits, err := GetReleaseCredits(db, release.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Credits: %s", err)
|
||||||
|
}
|
||||||
|
for _, credit := range credits {
|
||||||
|
release.Credits = append(release.Credits, credit)
|
||||||
|
}
|
||||||
|
|
||||||
|
if full {
|
||||||
|
// get tracks
|
||||||
|
tracks, err := GetReleaseTracks(db, release.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Tracks: %s", err)
|
||||||
|
}
|
||||||
|
for _, track := range tracks {
|
||||||
|
release.Tracks = append(release.Tracks, track)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get links
|
||||||
|
links, err := GetReleaseLinks(db, release.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Links: %s", err)
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
release.Links = append(release.Links, link)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases, nil
|
||||||
|
}
|
||||||
|
func GetReleaseCount(db *sqlx.DB, onlyVisible bool) (int, error) {
|
||||||
|
query := "SELECT count(*) FROM musicrelease"
|
||||||
|
if onlyVisible {
|
||||||
|
query += " WHERE visible=true"
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int
|
||||||
|
err := db.Get(&count, query)
|
||||||
|
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"INSERT INTO musicrelease "+
|
||||||
|
"(id, visible, title, description, type, release_date, artwork, buyname, buylink, copyright, copyrighturl) "+
|
||||||
|
"VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)",
|
||||||
|
release.ID,
|
||||||
|
release.Visible,
|
||||||
|
release.Title,
|
||||||
|
release.Description,
|
||||||
|
release.ReleaseType,
|
||||||
|
release.ReleaseDate.Format("2006-01-02 15:04:05"),
|
||||||
|
release.Artwork,
|
||||||
|
release.Buyname,
|
||||||
|
release.Buylink,
|
||||||
|
release.Copyright,
|
||||||
|
release.CopyrightURL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateRelease(db *sqlx.DB, release *model.Release) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"UPDATE musicrelease SET "+
|
||||||
|
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9, copyright=$10, copyrighturl=$11 "+
|
||||||
|
"WHERE id=$1",
|
||||||
|
release.ID,
|
||||||
|
release.Visible,
|
||||||
|
release.Title,
|
||||||
|
release.Description,
|
||||||
|
release.ReleaseType,
|
||||||
|
release.ReleaseDate.Format("2006-01-02 15:04:05"),
|
||||||
|
release.Artwork,
|
||||||
|
release.Buyname,
|
||||||
|
release.Buylink,
|
||||||
|
release.Copyright,
|
||||||
|
release.CopyrightURL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateReleaseTracks(db *sqlx.DB, releaseID string, new_tracks []string) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec("DELETE FROM musicreleasetrack WHERE release=$1", releaseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for i, trackID := range new_tracks {
|
||||||
|
_, err = tx.Exec(
|
||||||
|
"INSERT INTO musicreleasetrack "+
|
||||||
|
"(release, track, number) "+
|
||||||
|
"VALUES ($1, $2, $3)",
|
||||||
|
releaseID,
|
||||||
|
trackID,
|
||||||
|
i)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateReleaseCredits(db *sqlx.DB, releaseID string, new_credits []*model.Credit) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec("DELETE FROM musiccredit WHERE release=$1", releaseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, credit := range new_credits {
|
||||||
|
_, err = tx.Exec(
|
||||||
|
"INSERT INTO musiccredit "+
|
||||||
|
"(release, artist, role, is_primary) "+
|
||||||
|
"VALUES ($1, $2, $3, $4)",
|
||||||
|
releaseID,
|
||||||
|
credit.Artist.ID,
|
||||||
|
credit.Role,
|
||||||
|
credit.Primary,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateReleaseLinks(db *sqlx.DB, releaseID string, new_links []*model.Link) error {
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec("DELETE FROM musiclink WHERE release=$1", releaseID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, link := range new_links {
|
||||||
|
_, err := tx.Exec(
|
||||||
|
"INSERT INTO musiclink "+
|
||||||
|
"(release, name, url) "+
|
||||||
|
"VALUES ($1, $2, $3)",
|
||||||
|
releaseID,
|
||||||
|
link.Name,
|
||||||
|
link.URL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteRelease(db *sqlx.DB, releaseID string) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"DELETE FROM musicrelease "+
|
||||||
|
"WHERE id=$1",
|
||||||
|
releaseID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReleaseTracks(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
|
||||||
|
var tracks = []*model.Track{}
|
||||||
|
|
||||||
|
err := db.Select(&tracks,
|
||||||
|
"SELECT musictrack.* FROM musictrack "+
|
||||||
|
"JOIN musicreleasetrack ON track=id "+
|
||||||
|
"WHERE release=$1 "+
|
||||||
|
"ORDER BY number ASC",
|
||||||
|
releaseID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReleaseCredits(db *sqlx.DB, releaseID string) ([]*model.Credit, error) {
|
||||||
|
rows, err := db.Query(
|
||||||
|
"SELECT artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+
|
||||||
|
"FROM musiccredit "+
|
||||||
|
"JOIN artist ON artist=artist.id "+
|
||||||
|
"JOIN musicrelease ON release=musicrelease.id "+
|
||||||
|
"WHERE musicrelease.id=$1 "+
|
||||||
|
"ORDER BY is_primary DESC",
|
||||||
|
releaseID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var credits []*model.Credit
|
||||||
|
for rows.Next() {
|
||||||
|
credit := model.Credit{}
|
||||||
|
rows.Scan(
|
||||||
|
&credit.Artist.ID,
|
||||||
|
&credit.Artist.Name,
|
||||||
|
&credit.Artist.Website,
|
||||||
|
&credit.Artist.Avatar,
|
||||||
|
&credit.Role,
|
||||||
|
&credit.Primary)
|
||||||
|
credits = append(credits, &credit)
|
||||||
|
}
|
||||||
|
|
||||||
|
return credits, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetReleaseLinks(db *sqlx.DB, releaseID string) ([]*model.Link, error) {
|
||||||
|
var links = []*model.Link{}
|
||||||
|
|
||||||
|
err := db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", releaseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return links, nil
|
||||||
|
}
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"arimelody-web/model"
|
"arimelody-web/log"
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const TOKEN_LEN = 64
|
const TOKEN_LEN = 64
|
||||||
|
|
||||||
func GetSessionFromRequest(app *app.AppState, r *http.Request) (*model.Session, error) {
|
func GetSessionFromRequest(app *model.AppState, r *http.Request) (*model.Session, error) {
|
||||||
sessionCookie, err := r.Cookie(model.COOKIE_TOKEN)
|
sessionCookie, err := r.Cookie(model.COOKIE_TOKEN)
|
||||||
if err != nil && err != http.ErrNoCookie {
|
if err != nil && err != http.ErrNoCookie {
|
||||||
return nil, fmt.Errorf("Failed to retrieve session cookie: %v", err)
|
return nil, fmt.Errorf("Failed to retrieve session cookie: %v", err)
|
||||||
|
|
@ -25,7 +25,7 @@ func GetSessionFromRequest(app *app.AppState, r *http.Request) (*model.Session,
|
||||||
|
|
||||||
if sessionCookie != nil {
|
if sessionCookie != nil {
|
||||||
// fetch existing session
|
// fetch existing session
|
||||||
session, err = GetSession(app, sessionCookie.Value)
|
session, err = GetSession(app.DB, sessionCookie.Value)
|
||||||
|
|
||||||
if err != nil && !strings.Contains(err.Error(), "no rows") {
|
if err != nil && !strings.Contains(err.Error(), "no rows") {
|
||||||
return nil, fmt.Errorf("Failed to retrieve session: %v", err)
|
return nil, fmt.Errorf("Failed to retrieve session: %v", err)
|
||||||
|
|
@ -35,13 +35,13 @@ func GetSessionFromRequest(app *app.AppState, r *http.Request) (*model.Session,
|
||||||
if session.UserAgent != r.UserAgent() {
|
if session.UserAgent != r.UserAgent() {
|
||||||
msg := "Session user agent mismatch. A cookie may have been hijacked!"
|
msg := "Session user agent mismatch. A cookie may have been hijacked!"
|
||||||
if session.Account != nil {
|
if session.Account != nil {
|
||||||
account, _ := app.AccountService.GetByID(session.Account.ID)
|
account, _ := GetAccountByID(app.DB, session.Account.ID)
|
||||||
msg += " (Account \"" + account.Username + "\")"
|
msg += " (Account \"" + account.Username + "\")"
|
||||||
}
|
}
|
||||||
app.LogService.Warn(model.LOG_ACCOUNT, msg)
|
app.Log.Warn(log.TYPE_ACCOUNT, msg)
|
||||||
err = DeleteSession(app.DB, session.Token)
|
err = DeleteSession(app.DB, session.Token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
app.LogService.Warn(model.LOG_ACCOUNT, "Failed to delete affected session")
|
app.Log.Warn(log.TYPE_ACCOUNT, "Failed to delete affected session")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
@ -137,7 +137,7 @@ func SetSessionError(db *sqlx.DB, session *model.Session, message string) error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetSession(app *app.AppState, token string) (*model.Session, error) {
|
func GetSession(db *sqlx.DB, token string) (*model.Session, error) {
|
||||||
type dbSession struct {
|
type dbSession struct {
|
||||||
model.Session
|
model.Session
|
||||||
AttemptAccountID sql.NullString `db:"attempt_account"`
|
AttemptAccountID sql.NullString `db:"attempt_account"`
|
||||||
|
|
@ -145,7 +145,7 @@ func GetSession(app *app.AppState, token string) (*model.Session, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
session := dbSession{}
|
session := dbSession{}
|
||||||
err := app.DB.Get(
|
err := db.Get(
|
||||||
&session,
|
&session,
|
||||||
"SELECT * FROM session WHERE token=$1",
|
"SELECT * FROM session WHERE token=$1",
|
||||||
token,
|
token,
|
||||||
|
|
@ -155,14 +155,14 @@ func GetSession(app *app.AppState, token string) (*model.Session, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.AccountID.Valid {
|
if session.AccountID.Valid {
|
||||||
session.Account, err = app.AccountService.GetByID(session.AccountID.String)
|
session.Account, err = GetAccountByID(db, session.AccountID.String)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if session.AttemptAccountID.Valid {
|
if session.AttemptAccountID.Valid {
|
||||||
session.AttemptAccount, err = app.AccountService.GetByID(session.AttemptAccountID.String)
|
session.AttemptAccount, err = GetAccountByID(db, session.AttemptAccountID.String)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
181
controller/track.go
Normal file
181
controller/track.go
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"arimelody-web/model"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DATABASE
|
||||||
|
|
||||||
|
func GetTrack(db *sqlx.DB, id string) (*model.Track, error) {
|
||||||
|
var track = model.Track{}
|
||||||
|
|
||||||
|
stmt, _ := db.Preparex("SELECT * FROM musictrack WHERE id=$1")
|
||||||
|
err := stmt.Get(&track, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &track, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetAllTracks(db *sqlx.DB) ([]*model.Track, error) {
|
||||||
|
var tracks = []*model.Track{}
|
||||||
|
|
||||||
|
err := db.Select(&tracks, "SELECT * FROM musictrack")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
func GetTrackCount(db *sqlx.DB) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := db.Get(&count, "SELECT count(*) FROM musictrack")
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetOrphanTracks(db *sqlx.DB) ([]*model.Track, error) {
|
||||||
|
var tracks = []*model.Track{}
|
||||||
|
|
||||||
|
err := db.Select(&tracks, "SELECT * FROM musictrack WHERE id NOT IN (SELECT track FROM musicreleasetrack)")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTracksNotOnRelease(db *sqlx.DB, releaseID string) ([]*model.Track, error) {
|
||||||
|
var tracks = []*model.Track{}
|
||||||
|
|
||||||
|
err := db.Select(&tracks,
|
||||||
|
"SELECT * FROM musictrack "+
|
||||||
|
"WHERE id NOT IN "+
|
||||||
|
"(SELECT track FROM musicreleasetrack WHERE release=$1)",
|
||||||
|
releaseID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTrackReleases(db *sqlx.DB, trackID string, full bool) ([]*model.Release, error) {
|
||||||
|
var releases = []*model.Release{}
|
||||||
|
|
||||||
|
err := db.Select(&releases,
|
||||||
|
"SELECT id,title,type,release_date,artwork,buylink "+
|
||||||
|
"FROM musicrelease "+
|
||||||
|
"JOIN musicreleasetrack ON release=id "+
|
||||||
|
"WHERE track=$1 "+
|
||||||
|
"ORDER BY release_date",
|
||||||
|
trackID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamePrimary struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Primary bool `json:"primary" db:"is_primary"`
|
||||||
|
}
|
||||||
|
for _, release := range releases {
|
||||||
|
// get artists
|
||||||
|
credits := []NamePrimary{}
|
||||||
|
err := db.Select(&credits,
|
||||||
|
"SELECT name,is_primary FROM artist "+
|
||||||
|
"JOIN musiccredit ON artist=artist.id "+
|
||||||
|
"JOIN musicrelease ON release=musicrelease.id "+
|
||||||
|
"WHERE musicrelease.id=$1", release.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, credit := range credits {
|
||||||
|
release.Credits = append(release.Credits, &model.Credit{
|
||||||
|
Artist: model.Artist{
|
||||||
|
Name: credit.Name,
|
||||||
|
},
|
||||||
|
Primary: credit.Primary,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// get tracks
|
||||||
|
tracks := []string{}
|
||||||
|
err = db.Select(&tracks, "SELECT track FROM musicreleasetrack WHERE release=$1", release.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, trackID := range tracks {
|
||||||
|
release.Tracks = append(release.Tracks, &model.Track{
|
||||||
|
ID: trackID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func PullOrphanTracks(db *sqlx.DB) ([]*model.Track, error) {
|
||||||
|
var tracks = []*model.Track{}
|
||||||
|
|
||||||
|
err := db.Select(&tracks,
|
||||||
|
"SELECT id, title, description, lyrics, preview_url FROM musictrack "+
|
||||||
|
"WHERE id NOT IN "+
|
||||||
|
"(SELECT track FROM musicreleasetrack)",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tracks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func CreateTrack(db *sqlx.DB, track *model.Track) (string, error) {
|
||||||
|
var trackID string
|
||||||
|
err := db.QueryRow(
|
||||||
|
"INSERT INTO musictrack (title, description, lyrics, preview_url) "+
|
||||||
|
"VALUES ($1, $2, $3, $4) "+
|
||||||
|
"RETURNING id",
|
||||||
|
track.Title,
|
||||||
|
track.Description,
|
||||||
|
track.Lyrics,
|
||||||
|
track.PreviewURL,
|
||||||
|
).Scan(&trackID)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return trackID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func UpdateTrack(db *sqlx.DB, track *model.Track) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"UPDATE musictrack "+
|
||||||
|
"SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+
|
||||||
|
"WHERE id=$1",
|
||||||
|
track.ID,
|
||||||
|
track.Title,
|
||||||
|
track.Description,
|
||||||
|
track.Lyrics,
|
||||||
|
track.PreviewURL,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTrack(db *sqlx.DB, trackID string) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"DELETE FROM musictrack "+
|
||||||
|
"WHERE id=$1",
|
||||||
|
trackID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/twitch"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -12,13 +11,13 @@ import (
|
||||||
|
|
||||||
const TWITCH_API_BASE = "https://api.twitch.tv/helix/"
|
const TWITCH_API_BASE = "https://api.twitch.tv/helix/"
|
||||||
|
|
||||||
func TwitchSetup(app *app.AppState) error {
|
func TwitchSetup(app *model.AppState) error {
|
||||||
app.Twitch = &twitch.State{}
|
app.Twitch = &model.TwitchState{}
|
||||||
err := RefreshTwitchToken(app)
|
err := RefreshTwitchToken(app)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func RefreshTwitchToken(app *app.AppState) error {
|
func RefreshTwitchToken(app *model.AppState) error {
|
||||||
if app.Twitch != nil && app.Twitch.Token != nil && time.Now().UTC().After(app.Twitch.Token.ExpiresAt) {
|
if app.Twitch != nil && app.Twitch.Token != nil && time.Now().UTC().After(app.Twitch.Token.ExpiresAt) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -46,7 +45,7 @@ func RefreshTwitchToken(app *app.AppState) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
app.Twitch.Token = &twitch.OAuthToken{
|
app.Twitch.Token = &model.TwitchOAuthToken{
|
||||||
AccessToken: oauthResponse.AccessToken,
|
AccessToken: oauthResponse.AccessToken,
|
||||||
ExpiresAt: time.Now().UTC().Add(time.Second * time.Duration(oauthResponse.ExpiresIn)).UTC(),
|
ExpiresAt: time.Now().UTC().Add(time.Second * time.Duration(oauthResponse.ExpiresIn)).UTC(),
|
||||||
TokenType: oauthResponse.TokenType,
|
TokenType: oauthResponse.TokenType,
|
||||||
|
|
@ -55,10 +54,10 @@ func RefreshTwitchToken(app *app.AppState) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var lastStreamState *twitch.StreamInfo
|
var lastStreamState *model.TwitchStreamInfo
|
||||||
var lastStreamStateAt time.Time
|
var lastStreamStateAt time.Time
|
||||||
|
|
||||||
func GetTwitchStatus(app *app.AppState, broadcaster string) (*twitch.StreamInfo, error) {
|
func GetTwitchStatus(app *model.AppState, broadcaster string) (*model.TwitchStreamInfo, error) {
|
||||||
if lastStreamState != nil && time.Now().UTC().Before(lastStreamStateAt.Add(time.Minute)) {
|
if lastStreamState != nil && time.Now().UTC().Before(lastStreamStateAt.Add(time.Minute)) {
|
||||||
return lastStreamState, nil
|
return lastStreamState, nil
|
||||||
}
|
}
|
||||||
|
|
@ -77,7 +76,7 @@ func GetTwitchStatus(app *app.AppState, broadcaster string) (*twitch.StreamInfo,
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamsResponse struct {
|
type StreamsResponse struct {
|
||||||
Data []twitch.StreamInfo `json:"data"`
|
Data []model.TwitchStreamInfo `json:"data"`
|
||||||
}
|
}
|
||||||
streamInfo := StreamsResponse{}
|
streamInfo := StreamsResponse{}
|
||||||
err = json.NewDecoder(res.Body).Decode(&streamInfo)
|
err = json.NewDecoder(res.Body).Decode(&streamInfo)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
package cursor
|
package cursor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CursorClient struct {
|
type CursorClient struct {
|
||||||
|
|
@ -49,7 +49,7 @@ var clients = make(map[int32]*CursorClient)
|
||||||
var broadcast = make(chan CursorMessage)
|
var broadcast = make(chan CursorMessage)
|
||||||
var mutex = &sync.Mutex{}
|
var mutex = &sync.Mutex{}
|
||||||
|
|
||||||
func StartCursor(app *app.AppState) {
|
func StartCursor(app *model.AppState) {
|
||||||
var includes = func (clients []*CursorClient, client *CursorClient) bool {
|
var includes = func (clients []*CursorClient, client *CursorClient) bool {
|
||||||
for _, c := range clients {
|
for _, c := range clients {
|
||||||
if c.ID == client.ID { return true }
|
if c.ID == client.ID { return true }
|
||||||
|
|
@ -145,7 +145,7 @@ func handleClient(client *CursorClient) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Handler(app *app.AppState) http.HandlerFunc {
|
func Handler(app *model.AppState) http.HandlerFunc {
|
||||||
var upgrader = websocket.Upgrader{
|
var upgrader = websocket.Upgrader{
|
||||||
CheckOrigin: func (r *http.Request) bool {
|
CheckOrigin: func (r *http.Request) bool {
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
package discord
|
package discord
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const API_ENDPOINT = "https://discord.com/api/v10"
|
const API_ENDPOINT = "https://discord.com/api/v10"
|
||||||
|
|
@ -47,7 +47,7 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetOAuthTokenFromCode(app *app.AppState, code string) (string, error) {
|
func GetOAuthTokenFromCode(app *model.AppState, code string) (string, error) {
|
||||||
// let's get an oauth token!
|
// let's get an oauth token!
|
||||||
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/oauth2/token", API_ENDPOINT),
|
req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/oauth2/token", API_ENDPOINT),
|
||||||
strings.NewReader(url.Values{
|
strings.NewReader(url.Values{
|
||||||
|
|
@ -99,7 +99,7 @@ func GetOAuthCallbackURI(baseURL string) string {
|
||||||
return fmt.Sprintf("%s/admin/login", baseURL)
|
return fmt.Sprintf("%s/admin/login", baseURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRedirectURI(app *app.AppState) string {
|
func GetRedirectURI(app *model.AppState) string {
|
||||||
return fmt.Sprintf(
|
return fmt.Sprintf(
|
||||||
"https://discord.com/oauth2/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=identify",
|
"https://discord.com/oauth2/authorize?client_id=%s&response_type=code&redirect_uri=%s&scope=identify",
|
||||||
app.Config.Discord.ClientID,
|
app.Config.Discord.ClientID,
|
||||||
|
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package errors
|
|
||||||
|
|
||||||
type NotExistError struct {
|
|
||||||
query string
|
|
||||||
}
|
|
||||||
func NewNotExistError(query string) *NotExistError {
|
|
||||||
return &NotExistError{ query: query }
|
|
||||||
}
|
|
||||||
func (err *NotExistError) Error() string {
|
|
||||||
return err.query
|
|
||||||
}
|
|
||||||
func IsNotExistError(err error) bool {
|
|
||||||
_, ok := err.(*NotExistError)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package errors_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
goErrors "errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_NotExistError(t *testing.T) {
|
|
||||||
var err error
|
|
||||||
message := "entity does not exist"
|
|
||||||
|
|
||||||
t.Run("can create error", func(t *testing.T) {
|
|
||||||
err = errors.NewNotExistError(message)
|
|
||||||
assert.Error(t, err, message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("validator returns true for valid error", func(t *testing.T) {
|
|
||||||
assert.Equal(t, errors.IsNotExistError(err), true)
|
|
||||||
})
|
|
||||||
t.Run("validator returns false for invalid error", func(t *testing.T) {
|
|
||||||
assert.Equal(t, errors.IsNotExistError(goErrors.New("other error")), false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
package errors
|
|
||||||
|
|
||||||
type ValidationError struct {
|
|
||||||
message string
|
|
||||||
}
|
|
||||||
func NewValidationError(message string) *ValidationError {
|
|
||||||
return &ValidationError{ message: message }
|
|
||||||
}
|
|
||||||
func (err *ValidationError) Error() string {
|
|
||||||
return err.message
|
|
||||||
}
|
|
||||||
func IsValidationError(err error) bool {
|
|
||||||
_, ok := err.(*ValidationError)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
package errors_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
goErrors "errors"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_ValidationError(t *testing.T) {
|
|
||||||
var err error
|
|
||||||
message := "invalid input"
|
|
||||||
|
|
||||||
t.Run("can create error", func(t *testing.T) {
|
|
||||||
err = errors.NewValidationError(message)
|
|
||||||
assert.Error(t, err, message)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("validator returns true for valid error", func(t *testing.T) {
|
|
||||||
assert.Equal(t, errors.IsValidationError(err), true)
|
|
||||||
})
|
|
||||||
t.Run("validator returns false for invalid error", func(t *testing.T) {
|
|
||||||
assert.Equal(t, errors.IsValidationError(goErrors.New("other error")), false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -10,9 +10,7 @@ require (
|
||||||
require golang.org/x/crypto v0.27.0 // indirect
|
require golang.org/x/crypto v0.27.0 // indirect
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/go-cmp v0.5.9 // indirect
|
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -2,8 +2,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
|
|
@ -18,5 +16,3 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
|
||||||
|
|
|
||||||
143
log/log.go
Normal file
143
log/log.go
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
package log
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
Logger struct {
|
||||||
|
DB *sqlx.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
Log struct {
|
||||||
|
ID string `json:"id" db:"id"`
|
||||||
|
Level LogLevel `json:"level" db:"level"`
|
||||||
|
Type string `json:"type" db:"type"`
|
||||||
|
Content string `json:"content" db:"content"`
|
||||||
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
TYPE_ACCOUNT string = "account"
|
||||||
|
TYPE_MUSIC string = "music"
|
||||||
|
TYPE_ARTIST string = "artist"
|
||||||
|
TYPE_BLOG string = "blog"
|
||||||
|
TYPE_ARTWORK string = "artwork"
|
||||||
|
TYPE_FILES string = "files"
|
||||||
|
TYPE_MISC string = "misc"
|
||||||
|
TYPE_CURSOR string = "cursor"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LogLevel int
|
||||||
|
const (
|
||||||
|
LEVEL_INFO LogLevel = 0
|
||||||
|
LEVEL_WARN LogLevel = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
const DEFAULT_LOG_PAGE_LENGTH = 25
|
||||||
|
|
||||||
|
func (self *Logger) Info(logType string, format string, args ...any) {
|
||||||
|
logString := fmt.Sprintf(format, args...)
|
||||||
|
fmt.Printf("[%s] [%s] INFO: %s\n", time.Now().Format(time.UnixDate), logType, logString)
|
||||||
|
err := createLog(self.DB, LEVEL_INFO, logType, logString)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to push log to database: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Logger) Warn(logType string, format string, args ...any) {
|
||||||
|
logString := fmt.Sprintf(format, args...)
|
||||||
|
fmt.Fprintf(os.Stderr, "[%s] [%s] WARN: %s\n", time.Now().Format(time.UnixDate), logType, logString)
|
||||||
|
err := createLog(self.DB, LEVEL_WARN, logType, logString)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "WARN: Failed to push log to database: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Logger) Fetch(id string) (*Log, error) {
|
||||||
|
log := Log{}
|
||||||
|
err := self.DB.Get(&log, "SELECT * FROM auditlog WHERE id=$1", id)
|
||||||
|
return &log, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (self *Logger) Search(levelFilters []LogLevel, typeFilters []string, content string, limit int, offset int) ([]*Log, error) {
|
||||||
|
logs := []*Log{}
|
||||||
|
|
||||||
|
params := []any{ limit, offset }
|
||||||
|
conditions := ""
|
||||||
|
|
||||||
|
if len(content) > 0 {
|
||||||
|
content = "%" + content + "%"
|
||||||
|
conditions += " WHERE content LIKE $3"
|
||||||
|
params = append(params, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(levelFilters) > 0 {
|
||||||
|
if len(conditions) > 0 {
|
||||||
|
conditions += " AND level IN ("
|
||||||
|
} else {
|
||||||
|
conditions += " WHERE level IN ("
|
||||||
|
}
|
||||||
|
for i := range levelFilters {
|
||||||
|
conditions += fmt.Sprintf("$%d", len(params) + 1)
|
||||||
|
if i < len(levelFilters) - 1 {
|
||||||
|
conditions += ","
|
||||||
|
}
|
||||||
|
params = append(params, levelFilters[i])
|
||||||
|
}
|
||||||
|
conditions += ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(typeFilters) > 0 {
|
||||||
|
if len(conditions) > 0 {
|
||||||
|
conditions += " AND type IN ("
|
||||||
|
} else {
|
||||||
|
conditions += " WHERE type IN ("
|
||||||
|
}
|
||||||
|
for i := range typeFilters {
|
||||||
|
conditions += fmt.Sprintf("$%d", len(params) + 1)
|
||||||
|
if i < len(typeFilters) - 1 {
|
||||||
|
conditions += ","
|
||||||
|
}
|
||||||
|
params = append(params, typeFilters[i])
|
||||||
|
}
|
||||||
|
conditions += ")"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := fmt.Sprintf(
|
||||||
|
"SELECT * FROM auditlog%s ORDER BY created_at DESC LIMIT $1 OFFSET $2",
|
||||||
|
conditions,
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
fmt.Printf("%s (", query)
|
||||||
|
for i, param := range params {
|
||||||
|
fmt.Print(param)
|
||||||
|
if i < len(params) - 1 {
|
||||||
|
fmt.Print(", ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Print(")\n")
|
||||||
|
*/
|
||||||
|
|
||||||
|
err := self.DB.Select(&logs, query, params...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return logs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createLog(db *sqlx.DB, logLevel LogLevel, logType string, content string) error {
|
||||||
|
_, err := db.Exec(
|
||||||
|
"INSERT INTO auditlog (level, type, content) VALUES ($1,$2,$3)",
|
||||||
|
logLevel,
|
||||||
|
logType,
|
||||||
|
content,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
324
main.go
324
main.go
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
stdLog "log"
|
stdLog "log"
|
||||||
"math"
|
"math"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
|
|
@ -22,19 +21,10 @@ import (
|
||||||
"arimelody-web/colour"
|
"arimelody-web/colour"
|
||||||
"arimelody-web/controller"
|
"arimelody-web/controller"
|
||||||
"arimelody-web/cursor"
|
"arimelody-web/cursor"
|
||||||
|
"arimelody-web/log"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
|
||||||
"arimelody-web/view"
|
"arimelody-web/view"
|
||||||
|
|
||||||
accountRepo "arimelody-web/repository/account"
|
|
||||||
logRepo "arimelody-web/repository/log"
|
|
||||||
musicRepo "arimelody-web/repository/music"
|
|
||||||
|
|
||||||
repo "arimelody-web/repository/postgres"
|
|
||||||
accountService "arimelody-web/service/account"
|
|
||||||
logService "arimelody-web/service/log"
|
|
||||||
musicService "arimelody-web/service/music"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
_ "github.com/lib/pq"
|
_ "github.com/lib/pq"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
@ -50,13 +40,9 @@ const HRT_DATE int64 = 1756478697
|
||||||
var publicFS embed.FS
|
var publicFS embed.FS
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// TODO: switch to a new logger. this one kinda sucks
|
fmt.Printf("made with <3 by ari melody\n\n")
|
||||||
// i'll be so forreal i might write my own
|
|
||||||
logger := log.New(os.Stderr, "main", model.DEFAULT_LOG_FLAGS)
|
|
||||||
|
|
||||||
logger.Print("made with <3 by ari melody\n\n")
|
app := model.AppState{
|
||||||
|
|
||||||
app := app.AppState{
|
|
||||||
Config: controller.GetConfig(),
|
Config: controller.GetConfig(),
|
||||||
Twitch: nil,
|
Twitch: nil,
|
||||||
PublicFS: publicFS,
|
PublicFS: publicFS,
|
||||||
|
|
@ -64,55 +50,44 @@ func main() {
|
||||||
|
|
||||||
// initialise database connection
|
// initialise database connection
|
||||||
if app.Config.DB.Host == "" {
|
if app.Config.DB.Host == "" {
|
||||||
logger.Fatalf("FATAL: db.host not provided! Exiting...\n")
|
fmt.Fprintf(os.Stderr, "FATAL: db.host not provided! Exiting...\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if app.Config.DB.Name == "" {
|
if app.Config.DB.Name == "" {
|
||||||
logger.Fatalf("FATAL: db.name not provided! Exiting...\n")
|
fmt.Fprintf(os.Stderr, "FATAL: db.name not provided! Exiting...\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if app.Config.DB.User == "" {
|
if app.Config.DB.User == "" {
|
||||||
logger.Fatalf("FATAL: db.user not provided! Exiting...\n")
|
fmt.Fprintf(os.Stderr, "FATAL: db.user not provided! Exiting...\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if app.Config.DB.Pass == "" {
|
if app.Config.DB.Pass == "" {
|
||||||
logger.Fatalf("FATAL: db.pass not provided! Exiting...\n")
|
fmt.Fprintf(os.Stderr, "FATAL: db.pass not provided! Exiting...\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
psqlDB, err := sqlx.Connect(
|
var err error
|
||||||
|
app.DB, err = sqlx.Connect(
|
||||||
"postgres",
|
"postgres",
|
||||||
fmt.Sprintf(
|
fmt.Sprintf(
|
||||||
"host=%s port=%d user=%s password='%s' dbname=%s sslmode=disable",
|
"host=%s port=%d user=%s dbname=%s password='%s' sslmode=disable",
|
||||||
app.Config.DB.Host,
|
app.Config.DB.Host,
|
||||||
app.Config.DB.Port,
|
app.Config.DB.Port,
|
||||||
app.Config.DB.User,
|
app.Config.DB.User,
|
||||||
app.Config.DB.Pass,
|
|
||||||
app.Config.DB.Name,
|
app.Config.DB.Name,
|
||||||
|
app.Config.DB.Pass,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("Failed to connect to database: %v", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Unable to initialise database: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer psqlDB.Close()
|
app.DB.SetConnMaxLifetime(time.Minute * 3)
|
||||||
psqlDB.SetConnMaxLifetime(time.Minute * 3)
|
app.DB.SetMaxOpenConns(10)
|
||||||
psqlDB.SetMaxOpenConns(10)
|
app.DB.SetMaxIdleConns(10)
|
||||||
psqlDB.SetMaxIdleConns(10)
|
defer app.DB.Close()
|
||||||
app.DB = psqlDB
|
|
||||||
|
|
||||||
logRepo := logRepo.NewLogRepositoryPostgres(psqlDB)
|
app.Log = log.Logger{ DB: app.DB }
|
||||||
app.LogService = logService.NewLogService(
|
|
||||||
logRepo,
|
|
||||||
log.New(os.Stderr, "logger", model.DEFAULT_LOG_FLAGS),
|
|
||||||
)
|
|
||||||
|
|
||||||
accountRepo := accountRepo.NewAccountRepositoryPostgres(psqlDB)
|
|
||||||
app.AccountService = accountService.NewAccountService(
|
|
||||||
accountRepo,
|
|
||||||
log.New(os.Stderr, "account-repo", model.DEFAULT_LOG_FLAGS),
|
|
||||||
)
|
|
||||||
|
|
||||||
musicRepo := musicRepo.NewMusicRepositoryPostgres(psqlDB)
|
|
||||||
app.MusicService = musicService.NewMusicService(
|
|
||||||
musicRepo,
|
|
||||||
log.New(os.Stderr, "music-repo", model.DEFAULT_LOG_FLAGS),
|
|
||||||
)
|
|
||||||
|
|
||||||
// handle command arguments
|
// handle command arguments
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
|
|
@ -121,18 +96,21 @@ func main() {
|
||||||
switch arg {
|
switch arg {
|
||||||
case "createTOTP":
|
case "createTOTP":
|
||||||
if len(os.Args) < 4 {
|
if len(os.Args) < 4 {
|
||||||
logger.Fatalf("FATAL: `username` and `name` must be specified for createTOTP.\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for createTOTP.\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
totpName := os.Args[3]
|
totpName := os.Args[3]
|
||||||
|
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account == nil {
|
if account == nil {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" does not exist.\n", username)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
|
secret := controller.GenerateTOTPSecret(controller.TOTP_SECRET_LENGTH)
|
||||||
|
|
@ -145,116 +123,133 @@ func main() {
|
||||||
err = controller.CreateTOTP(app.DB, &totp)
|
err = controller.CreateTOTP(app.DB, &totp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if strings.HasPrefix(err.Error(), "pq: duplicate key") {
|
if strings.HasPrefix(err.Error(), "pq: duplicate key") {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" already has a TOTP method named \"%s\"!\n", account.Username, totp.Name)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" already has a TOTP method named \"%s\"!\n", account.Username, totp.Name)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
logger.Fatalf("FATAL: Failed to create TOTP method: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP method: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "TOTP method \"%s\" for \"%s\" created via config utility.", totp.Name, account.Username)
|
app.Log.Info(log.TYPE_ACCOUNT, "TOTP method \"%s\" for \"%s\" created via config utility.", totp.Name, account.Username)
|
||||||
url := controller.GenerateTOTPURI(account.Username, totp.Secret)
|
url := controller.GenerateTOTPURI(account.Username, totp.Secret)
|
||||||
logger.Printf("%s\n", url)
|
fmt.Printf("%s\n", url)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "deleteTOTP":
|
case "deleteTOTP":
|
||||||
if len(os.Args) < 4 {
|
if len(os.Args) < 4 {
|
||||||
logger.Fatalf("FATAL: `username` and `name` must be specified for deleteTOTP.\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for deleteTOTP.\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
totpName := os.Args[3]
|
totpName := os.Args[3]
|
||||||
|
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account == nil {
|
if account == nil {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" does not exist.\n", username)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = controller.DeleteTOTP(app.DB, account.ID, totpName)
|
err = controller.DeleteTOTP(app.DB, account.ID, totpName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to create TOTP method: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP method: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "TOTP method \"%s\" for \"%s\" deleted via config utility.", totpName, account.Username)
|
app.Log.Info(log.TYPE_ACCOUNT, "TOTP method \"%s\" for \"%s\" deleted via config utility.", totpName, account.Username)
|
||||||
logger.Printf("TOTP method \"%s\" deleted.\n", totpName)
|
fmt.Printf("TOTP method \"%s\" deleted.\n", totpName)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "listTOTP":
|
case "listTOTP":
|
||||||
if len(os.Args) < 3 {
|
if len(os.Args) < 3 {
|
||||||
logger.Fatalf("FATAL: `username` must be specified for listTOTP.\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for listTOTP.\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
|
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account == nil {
|
if account == nil {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" does not exist.\n", username)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
totps, err := controller.GetTOTPsForAccount(app.DB, account.ID)
|
totps, err := controller.GetTOTPsForAccount(app.DB, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to create TOTP methods: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create TOTP methods: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, totp := range totps {
|
for i, totp := range totps {
|
||||||
logger.Printf("%d. %s - Created %s\n", i + 1, totp.Name, totp.CreatedAt)
|
fmt.Printf("%d. %s - Created %s\n", i + 1, totp.Name, totp.CreatedAt)
|
||||||
}
|
}
|
||||||
if len(totps) == 0 {
|
if len(totps) == 0 {
|
||||||
logger.Printf("\"%s\" has no TOTP methods.\n", account.Username)
|
fmt.Printf("\"%s\" has no TOTP methods.\n", account.Username)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|
||||||
case "testTOTP":
|
case "testTOTP":
|
||||||
if len(os.Args) < 4 {
|
if len(os.Args) < 4 {
|
||||||
logger.Fatalf("FATAL: `username` and `name` must be specified for testTOTP.\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` and `name` must be specified for testTOTP.\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
totpName := os.Args[3]
|
totpName := os.Args[3]
|
||||||
|
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account == nil {
|
if account == nil {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" does not exist.\n", username)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
totp, err := controller.GetTOTP(app.DB, account.ID, totpName)
|
totp, err := controller.GetTOTP(app.DB, account.ID, totpName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch TOTP method \"%s\": %v\n", totpName, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch TOTP method \"%s\": %v\n", totpName, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if totp == nil {
|
if totp == nil {
|
||||||
logger.Fatalf("FATAL: TOTP method \"%s\" does not exist for account \"%s\"\n", totpName, username)
|
fmt.Fprintf(os.Stderr, "FATAL: TOTP method \"%s\" does not exist for account \"%s\"\n", totpName, username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
code := controller.GenerateTOTP(totp.Secret, 0)
|
code := controller.GenerateTOTP(totp.Secret, 0)
|
||||||
logger.Printf("%s\n", code)
|
fmt.Printf("%s\n", code)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "cleanTOTP":
|
case "cleanTOTP":
|
||||||
err := controller.DeleteUnconfirmedTOTPs(app.DB)
|
err := controller.DeleteUnconfirmedTOTPs(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to clean up TOTP methods: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up TOTP methods: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "TOTP methods pruned via config utility.")
|
app.Log.Info(log.TYPE_ACCOUNT, "TOTP methods pruned via config utility.")
|
||||||
logger.Printf("Cleaned up dangling TOTP methods successfully.\n")
|
fmt.Printf("Cleaned up dangling TOTP methods successfully.\n")
|
||||||
return
|
return
|
||||||
|
|
||||||
case "createInvite":
|
case "createInvite":
|
||||||
logger.Printf("Creating invite...\n")
|
fmt.Printf("Creating invite...\n")
|
||||||
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
|
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to create invite code: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "Invite generted via config utility (%s).", invite.Code)
|
app.Log.Info(log.TYPE_ACCOUNT, "Invite generted via config utility (%s).", invite.Code)
|
||||||
logger.Printf(
|
fmt.Printf(
|
||||||
"Here you go! This code expires in %d hours: %s\n",
|
"Here you go! This code expires in %d hours: %s\n",
|
||||||
int(math.Ceil(invite.ExpiresAt.Sub(invite.CreatedAt).Hours())),
|
int(math.Ceil(invite.ExpiresAt.Sub(invite.CreatedAt).Hours())),
|
||||||
invite.Code,
|
invite.Code,
|
||||||
|
|
@ -262,26 +257,28 @@ func main() {
|
||||||
return
|
return
|
||||||
|
|
||||||
case "purgeInvites":
|
case "purgeInvites":
|
||||||
logger.Printf("Deleting all invites...\n")
|
fmt.Printf("Deleting all invites...\n")
|
||||||
err := controller.DeleteAllInvites(app.DB)
|
err := controller.DeleteAllInvites(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to delete invites: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to delete invites: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "Invites purged via config utility.")
|
app.Log.Info(log.TYPE_ACCOUNT, "Invites purged via config utility.")
|
||||||
logger.Printf("Invites deleted successfully.\n")
|
fmt.Printf("Invites deleted successfully.\n")
|
||||||
return
|
return
|
||||||
|
|
||||||
case "listAccounts":
|
case "listAccounts":
|
||||||
accounts, err := app.AccountService.GetAll()
|
accounts, err := controller.GetAllAccounts(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch accounts: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch accounts: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, account := range accounts {
|
for _, account := range accounts {
|
||||||
email := "<none>"
|
email := "<none>"
|
||||||
if account.Email.Valid { email = account.Email.String }
|
if account.Email.Valid { email = account.Email.String }
|
||||||
logger.Printf(
|
fmt.Printf(
|
||||||
"User: %s\n" +
|
"User: %s\n" +
|
||||||
"\tID: %s\n" +
|
"\tID: %s\n" +
|
||||||
"\tEmail: %s\n" +
|
"\tEmail: %s\n" +
|
||||||
|
|
@ -298,132 +295,150 @@ func main() {
|
||||||
|
|
||||||
case "changePassword":
|
case "changePassword":
|
||||||
if len(os.Args) < 4 {
|
if len(os.Args) < 4 {
|
||||||
logger.Fatalf("FATAL: `username` and `password` must be specified for changePassword\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` and `password` must be specified for changePassword\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
password := os.Args[3]
|
password := os.Args[3]
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if account == nil {
|
if account == nil {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" does not exist.\n", username)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to update password: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to update password: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
account.Password = string(hashedPassword)
|
account.Password = string(hashedPassword)
|
||||||
|
err = controller.UpdateAccount(app.DB, account)
|
||||||
if err = app.AccountService.ChangePassword(account.ID, string(hashedPassword)); err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to update password: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to update password: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "Password for '%s' updated via config utility.", account.Username)
|
app.Log.Info(log.TYPE_ACCOUNT, "Password for '%s' updated via config utility.", account.Username)
|
||||||
logger.Printf("Password for \"%s\" updated successfully.\n", account.Username)
|
fmt.Printf("Password for \"%s\" updated successfully.\n", account.Username)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "deleteAccount":
|
case "deleteAccount":
|
||||||
if len(os.Args) < 3 {
|
if len(os.Args) < 3 {
|
||||||
logger.Fatalf("FATAL: `username` must be specified for deleteAccount\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for deleteAccount\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
logger.Printf("Deleting account \"%s\"...\n", username)
|
fmt.Printf("Deleting account \"%s\"...\n", username)
|
||||||
|
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account == nil {
|
if account == nil {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" does not exist.\n", username)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Printf("You are about to delete \"%s\". Are you sure? (y/[N]): ", account.Username)
|
fmt.Printf("You are about to delete \"%s\". Are you sure? (y/[N]): ", account.Username)
|
||||||
res := ""
|
res := ""
|
||||||
fmt.Scanln(&res)
|
fmt.Scanln(&res)
|
||||||
if !strings.HasPrefix(res, "y") {
|
if !strings.HasPrefix(res, "y") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.AccountService.Delete(account.ID)
|
err = controller.DeleteAccount(app.DB, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to delete account: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to delete account: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "Account '%s' deleted via config utility.", account.Username)
|
app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' deleted via config utility.", account.Username)
|
||||||
logger.Printf("Account \"%s\" deleted successfully.\n", account.Username)
|
fmt.Printf("Account \"%s\" deleted successfully.\n", account.Username)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "lockAccount":
|
case "lockAccount":
|
||||||
if len(os.Args) < 3 {
|
if len(os.Args) < 3 {
|
||||||
logger.Fatalf("FATAL: `username` must be specified for lockAccount\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for lockAccount\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
logger.Printf("Unlocking account \"%s\"...\n", username)
|
fmt.Printf("Unlocking account \"%s\"...\n", username)
|
||||||
|
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account == nil {
|
if account == nil {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" does not exist.\n", username)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.AccountService.Lock(account.ID)
|
err = controller.LockAccount(app.DB, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to lock account: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to lock account: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "Account '%s' locked via config utility.", account.Username)
|
app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' locked via config utility.", account.Username)
|
||||||
logger.Printf("Account \"%s\" locked successfully.\n", account.Username)
|
fmt.Printf("Account \"%s\" locked successfully.\n", account.Username)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "unlockAccount":
|
case "unlockAccount":
|
||||||
if len(os.Args) < 3 {
|
if len(os.Args) < 3 {
|
||||||
logger.Fatalf("FATAL: `username` must be specified for unlockAccount\n")
|
fmt.Fprintf(os.Stderr, "FATAL: `username` must be specified for unlockAccount\n")
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
username := os.Args[2]
|
username := os.Args[2]
|
||||||
logger.Printf("Unlocking account \"%s\"...\n", username)
|
fmt.Printf("Unlocking account \"%s\"...\n", username)
|
||||||
|
|
||||||
account, err := app.AccountService.GetByUsername(username)
|
account, err := controller.GetAccountByUsername(app.DB, username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch account \"%s\": %v\n", username, err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if account == nil {
|
if account == nil {
|
||||||
logger.Fatalf("FATAL: Account \"%s\" does not exist.\n", username)
|
fmt.Fprintf(os.Stderr, "FATAL: Account \"%s\" does not exist.\n", username)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = app.AccountService.Unlock(account.ID)
|
err = controller.UnlockAccount(app.DB, account.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to unlock account: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to unlock account: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
app.LogService.Info(model.LOG_ACCOUNT, "Account '%s' unlocked via config utility.", account.Username)
|
app.Log.Info(log.TYPE_ACCOUNT, "Account '%s' unlocked via config utility.", account.Username)
|
||||||
logger.Printf("Account \"%s\" unlocked successfully.\n", account.Username)
|
fmt.Printf("Account \"%s\" unlocked successfully.\n", account.Username)
|
||||||
return
|
return
|
||||||
|
|
||||||
case "logs":
|
case "logs":
|
||||||
// TODO: add log search parameters
|
// TODO: add log search parameters
|
||||||
logs, err := app.LogService.Search([]model.LogLevel{}, []string{}, "", 100, 0)
|
logs, err := app.Log.Search([]log.LogLevel{}, []string{}, "", 100, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to fetch logs: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to fetch logs: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
for _, item := range(logs) {
|
for _, item := range(logs) {
|
||||||
levelStr := ""
|
levelStr := ""
|
||||||
switch item.Level {
|
switch item.Level {
|
||||||
case model.LEVEL_INFO:
|
case log.LEVEL_INFO:
|
||||||
levelStr = "INFO"
|
levelStr = "INFO"
|
||||||
case model.LEVEL_WARN:
|
case log.LEVEL_WARN:
|
||||||
levelStr = "WARN"
|
levelStr = "WARN"
|
||||||
default:
|
default:
|
||||||
levelStr = fmt.Sprintf("? (%d)", item.Level)
|
levelStr = fmt.Sprintf("? (%d)", item.Level)
|
||||||
}
|
}
|
||||||
logger.Printf("[%s] %s:\n\t[%s] %s: %s\n", item.CreatedAt.Format(time.UnixDate), item.ID, item.Type, levelStr, item.Content)
|
fmt.Printf("[%s] %s:\n\t[%s] %s: %s\n", item.CreatedAt.Format(time.UnixDate), item.ID, item.Type, levelStr, item.Content)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -449,66 +464,68 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle DB migrations
|
// handle DB migrations
|
||||||
if psqlDB != nil {
|
controller.CheckDBVersionAndMigrate(app.DB)
|
||||||
repo.CheckDBVersionAndMigrate(psqlDB)
|
|
||||||
}
|
|
||||||
|
|
||||||
if app.Config.Twitch != nil {
|
if app.Config.Twitch != nil {
|
||||||
err = controller.TwitchSetup(&app)
|
err = controller.TwitchSetup(&app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Printf("WARN: Failed to set up Twitch integration: %v\n", err)
|
fmt.Fprintf(os.Stderr, "WARN: Failed to set up Twitch integration: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// initial invite code
|
// initial invite code
|
||||||
accountsCount, err := app.AccountService.GetCount()
|
accountsCount := 0
|
||||||
|
err = app.DB.Get(&accountsCount, "SELECT count(*) FROM account")
|
||||||
if err != nil { panic(err) }
|
if err != nil { panic(err) }
|
||||||
if accountsCount == 0 {
|
if accountsCount == 0 {
|
||||||
_, err := app.DB.Exec("DELETE FROM invite")
|
_, err := app.DB.Exec("DELETE FROM invite")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to clear existing invite codes: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear existing invite codes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
|
invite, err := controller.CreateInvite(app.DB, 16, time.Hour * 24)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to create invite code: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to create invite code: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Printf("No accounts exist! Generated invite code: %s\n", invite.Code)
|
fmt.Printf("No accounts exist! Generated invite code: %s\n", invite.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete expired sessions
|
// delete expired sessions
|
||||||
err = controller.DeleteExpiredSessions(app.DB)
|
err = controller.DeleteExpiredSessions(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to clear expired sessions: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired sessions: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete expired invites
|
// delete expired invites
|
||||||
err = controller.DeleteExpiredInvites(app.DB)
|
err = controller.DeleteExpiredInvites(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to clear expired invite codes: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clear expired invite codes: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// clean up unconfirmed TOTP methods
|
// clean up unconfirmed TOTP methods
|
||||||
err = controller.DeleteUnconfirmedTOTPs(app.DB)
|
err = controller.DeleteUnconfirmedTOTPs(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatalf("FATAL: Failed to clean up unconfirmed TOTP methods: %v\n", err)
|
fmt.Fprintf(os.Stderr, "FATAL: Failed to clean up unconfirmed TOTP methods: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
go cursor.StartCursor(&app)
|
go cursor.StartCursor(&app)
|
||||||
|
|
||||||
httpLogger := log.New(os.Stderr, "http", model.DEFAULT_LOG_FLAGS)
|
|
||||||
|
|
||||||
// start the web server!
|
// start the web server!
|
||||||
mux := createServeMux(&app)
|
mux := createServeMux(&app)
|
||||||
logger.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port)
|
fmt.Printf("Now serving at http://%s:%d\n", app.Config.Host, app.Config.Port)
|
||||||
stdLog.Fatal(
|
stdLog.Fatal(
|
||||||
http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
|
http.ListenAndServe(fmt.Sprintf("%s:%d", app.Config.Host, app.Config.Port),
|
||||||
CheckRequest(&app, httpLogger, HTTPLog(httpLogger, DefaultHeaders(mux))),
|
CheckRequest(&app, HTTPLog(DefaultHeaders(mux))),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
func createServeMux(app *app.AppState) *http.ServeMux {
|
func createServeMux(app *model.AppState) *http.ServeMux {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
|
mux.Handle("/admin/", http.StripPrefix("/admin", admin.Handler(app)))
|
||||||
|
|
@ -551,7 +568,7 @@ var PoweredByStrings = []string{
|
||||||
"30 billion dollars in VC funding",
|
"30 billion dollars in VC funding",
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckRequest(app *app.AppState, log *log.Logger, next http.Handler) http.Handler {
|
func CheckRequest(app *model.AppState, next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// requests with empty user agents are considered suspicious.
|
// requests with empty user agents are considered suspicious.
|
||||||
// every browser supplies them; hell, even curl supplies them.
|
// every browser supplies them; hell, even curl supplies them.
|
||||||
|
|
@ -568,7 +585,8 @@ func CheckRequest(app *app.AppState, log *log.Logger, next http.Handler) http.Ha
|
||||||
if strings.HasSuffix(r.URL.Path, ".php") ||
|
if strings.HasSuffix(r.URL.Path, ".php") ||
|
||||||
strings.HasSuffix(r.URL.Path, ".php7") {
|
strings.HasSuffix(r.URL.Path, ".php7") {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
log.Printf(
|
fmt.Fprintf(
|
||||||
|
os.Stderr,
|
||||||
"WARN: Suspicious activity blocked: {\"path\":\"%s\",\"address\":\"%s\"}\n",
|
"WARN: Suspicious activity blocked: {\"path\":\"%s\",\"address\":\"%s\"}\n",
|
||||||
r.URL.Path,
|
r.URL.Path,
|
||||||
r.RemoteAddr,
|
r.RemoteAddr,
|
||||||
|
|
@ -618,7 +636,7 @@ func (lrw *LoggingResponseWriter) WriteHeader(status int) {
|
||||||
lrw.ResponseWriter.WriteHeader(status)
|
lrw.ResponseWriter.WriteHeader(status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func HTTPLog(log *log.Logger, next http.Handler) http.Handler {
|
func HTTPLog(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
|
|
@ -640,7 +658,7 @@ func HTTPLog(log *log.Logger, next http.Handler) http.Handler {
|
||||||
if lrw.Status - 400 <= 0 { statusColour = colour.White }
|
if lrw.Status - 400 <= 0 { statusColour = colour.White }
|
||||||
if lrw.Status - 300 <= 0 { statusColour = colour.Green }
|
if lrw.Status - 300 <= 0 { statusColour = colour.Green }
|
||||||
|
|
||||||
log.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
|
fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n",
|
||||||
after.Format(time.UnixDate),
|
after.Format(time.UnixDate),
|
||||||
r.Method,
|
r.Method,
|
||||||
r.URL.Path,
|
r.URL.Path,
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ type (
|
||||||
Email sql.NullString `json:"email" db:"email"`
|
Email sql.NullString `json:"email" db:"email"`
|
||||||
AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"`
|
AvatarURL sql.NullString `json:"avatar_url" db:"avatar_url"`
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||||
FailAttempts int `json:"fail_attempts" db:"fail_attempts"`
|
FailAttempts int `json:"fail_attempts" db:"fail_attempts"`
|
||||||
Locked bool `json:"locked" db:"locked"`
|
Locked bool `json:"locked" db:"locked"`
|
||||||
|
|
||||||
Privileges []AccountPrivilege `json:"privileges"`
|
Privileges []AccountPrivilege `json:"privileges"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,11 @@
|
||||||
package app
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
"arimelody-web/model/twitch"
|
"arimelody-web/log"
|
||||||
|
|
||||||
logService "arimelody-web/service/log"
|
|
||||||
//inviteService "arimelody-web/service/invite"
|
|
||||||
accountService "arimelody-web/service/account"
|
|
||||||
//sessionService "arimelody-web/service/session"
|
|
||||||
musicService "arimelody-web/service/music"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
|
|
@ -49,13 +43,8 @@ type (
|
||||||
AppState struct {
|
AppState struct {
|
||||||
DB *sqlx.DB
|
DB *sqlx.DB
|
||||||
Config Config
|
Config Config
|
||||||
Twitch *twitch.State
|
Log log.Logger
|
||||||
|
Twitch *TwitchState
|
||||||
PublicFS embed.FS
|
PublicFS embed.FS
|
||||||
|
|
||||||
LogService *logService.LogService
|
|
||||||
//InviteService *inviteService.InviteService
|
|
||||||
AccountService *accountService.AccountService
|
|
||||||
//SesisonService *sessionService.SessionService
|
|
||||||
MusicService *musicService.MusicService
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -9,11 +9,9 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const DEFAULT_AVATAR_URL = "/img/default-avatar.png"
|
|
||||||
|
|
||||||
func (artist Artist) GetAvatar() string {
|
func (artist Artist) GetAvatar() string {
|
||||||
if artist.Avatar == "" {
|
if artist.Avatar == "" {
|
||||||
return DEFAULT_AVATAR_URL
|
return "/img/default-avatar.png"
|
||||||
}
|
}
|
||||||
return artist.Avatar
|
return artist.Avatar
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,21 @@
|
||||||
package model_test
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model"
|
"testing"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Artist_GetAvatar(t *testing.T) {
|
func Test_Artist_GetAvatar(t *testing.T) {
|
||||||
want := "testavatar.png"
|
want := "testavatar.png"
|
||||||
artist := model.Artist{ Avatar: want }
|
artist := Artist{ Avatar: want }
|
||||||
|
|
||||||
got := artist.GetAvatar()
|
got := artist.GetAvatar()
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf(`correct value not returned when avatar is populated (want "%s", got "%s")`, want, got)
|
t.Errorf(`correct value not returned when avatar is populated (want "%s", got "%s")`, want, got)
|
||||||
}
|
}
|
||||||
|
|
||||||
artist = model.Artist{}
|
artist = Artist{}
|
||||||
|
|
||||||
want = model.DEFAULT_AVATAR_URL
|
want = "/img/default-avatar.png"
|
||||||
got = artist.GetAvatar()
|
got = artist.GetAvatar()
|
||||||
if want != got {
|
if want != got {
|
||||||
t.Errorf(`default value not returned when avatar is empty (want "%s", got "%s")`, want, got)
|
t.Errorf(`default value not returned when avatar is empty (want "%s", got "%s")`, want, got)
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,9 @@ package model
|
||||||
|
|
||||||
type (
|
type (
|
||||||
Credit struct {
|
Credit struct {
|
||||||
Release *Release `json:"release"`
|
Release Release `json:"release"`
|
||||||
Artist *Artist `json:"artist"`
|
Artist Artist `json:"artist"`
|
||||||
Role string `json:"role"`
|
Role string `json:"role"`
|
||||||
Primary bool `json:"primary" db:"is_primary"`
|
Primary bool `json:"primary" db:"is_primary"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
package model_test
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model"
|
"testing"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Link_NormaliseName(t *testing.T) {
|
func Test_Link_NormaliseName(t *testing.T) {
|
||||||
link := model.Link{
|
link := Link{
|
||||||
Name: "!c@o#o$l%-^a&w*e(s)o_m=e+-[l{i]n}k-0123456789ABCDEF",
|
Name: "!c@o#o$l%-^a&w*e(s)o_m=e+-[l{i]n}k-0123456789ABCDEF",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
36
model/log.go
36
model/log.go
|
|
@ -1,36 +0,0 @@
|
||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
LogLevel int
|
|
||||||
|
|
||||||
Log struct {
|
|
||||||
ID string `json:"id" db:"id"`
|
|
||||||
Level LogLevel `json:"level" db:"level"`
|
|
||||||
Type string `json:"type" db:"type"`
|
|
||||||
Content string `json:"content" db:"content"`
|
|
||||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DEFAULT_LOG_FLAGS = log.Ldate | log.Ltime | log.Lmicroseconds
|
|
||||||
|
|
||||||
LOG_ACCOUNT string = "account"
|
|
||||||
LOG_MUSIC string = "music"
|
|
||||||
LOG_ARTIST string = "artist"
|
|
||||||
LOG_BLOG string = "blog"
|
|
||||||
LOG_ARTWORK string = "artwork"
|
|
||||||
LOG_FILES string = "files"
|
|
||||||
LOG_MISC string = "misc"
|
|
||||||
LOG_CURSOR string = "cursor"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
LEVEL_INFO LogLevel = 0
|
|
||||||
LEVEL_WARN LogLevel = 1
|
|
||||||
)
|
|
||||||
|
|
@ -34,8 +34,6 @@ const (
|
||||||
EP ReleaseType = "EP"
|
EP ReleaseType = "EP"
|
||||||
Compilation ReleaseType = "compilation"
|
Compilation ReleaseType = "compilation"
|
||||||
Upcoming ReleaseType = "upcoming"
|
Upcoming ReleaseType = "upcoming"
|
||||||
|
|
||||||
DEFAULT_RELEASE_ARTWORK_URL = "/img/default-cover-art.png"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GETTERS
|
// GETTERS
|
||||||
|
|
@ -54,7 +52,7 @@ func (release Release) PrintReleaseDate() string {
|
||||||
|
|
||||||
func (release Release) GetArtwork() string {
|
func (release Release) GetArtwork() string {
|
||||||
if release.Artwork == "" {
|
if release.Artwork == "" {
|
||||||
return DEFAULT_RELEASE_ARTWORK_URL
|
return "/img/default-cover-art.png"
|
||||||
}
|
}
|
||||||
return release.Artwork
|
return release.Artwork
|
||||||
}
|
}
|
||||||
|
|
@ -95,20 +93,3 @@ func (release Release) PrintArtists(only_primary bool, ampersand bool) string {
|
||||||
return strings.Join(names[:], ", ")
|
return strings.Join(names[:], ", ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidReleaseType(releaseType string) (ReleaseType, bool) {
|
|
||||||
switch releaseType {
|
|
||||||
case "single":
|
|
||||||
return Single, true
|
|
||||||
case "album":
|
|
||||||
return Album, true
|
|
||||||
case "EP":
|
|
||||||
return EP, true
|
|
||||||
case "compilation":
|
|
||||||
return Compilation, true
|
|
||||||
case "upcoming":
|
|
||||||
return Upcoming, true
|
|
||||||
default:
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,183 +1,157 @@
|
||||||
package model_test
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model"
|
"testing"
|
||||||
"strings"
|
"time"
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Release(t *testing.T) {
|
func Test_Release_DescriptionHTML(t *testing.T) {
|
||||||
t.Run("prints correct description HTML", func(t *testing.T) {
|
release := Release{
|
||||||
release := model.Release{
|
Description: "this is\na test\n<strong>description!</strong>",
|
||||||
Description: "this is\na test\n<strong>description!</strong>",
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// descriptions are set by privileged users,
|
// descriptions are set by privileged users,
|
||||||
// so we'll allow HTML injection here
|
// so we'll allow HTML injection here
|
||||||
assert.Equal(
|
want := "this is<br>a test<br><strong>description!</strong>"
|
||||||
t,
|
got := release.GetDescriptionHTML()
|
||||||
string(release.GetDescriptionHTML()),
|
if want != string(got) {
|
||||||
"this is<br>a test<br><strong>description!</strong>",
|
t.Errorf(`release description incorrectly formatted (want "%s", got "%s")`, want, got)
|
||||||
)
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
t.Run("prints correct release date", func(t *testing.T) {
|
func Test_Release_ReleaseDate(t *testing.T) {
|
||||||
release := model.Release{
|
release := Release{
|
||||||
ReleaseDate: time.Date(2025, time.July, 26, 16, 0, 0, 0, time.UTC),
|
ReleaseDate: time.Date(2025, time.July, 26, 16, 0, 0, 0, time.UTC),
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Equal(t, release.TextReleaseDate(), "2025-07-26T16:00")
|
want := "2025-07-26T16:00"
|
||||||
assert.Equal(t, release.PrintReleaseDate(), "26 July 2025")
|
got := release.TextReleaseDate()
|
||||||
})
|
if want != got {
|
||||||
|
t.Errorf(`release date incorrectly formatted (want "%s", got "%s")`, want, got)
|
||||||
t.Run("returns correct artwork", func(t *testing.T) {
|
}
|
||||||
artwork := "testartwork.png"
|
|
||||||
release := model.Release{ Artwork: artwork }
|
want = "26 July 2025"
|
||||||
assert.Equal(t, release.GetArtwork(), artwork)
|
got = release.PrintReleaseDate()
|
||||||
})
|
if want != got {
|
||||||
|
t.Errorf(`release date (print) incorrectly formatted (want "%s", got "%s")`, want, got)
|
||||||
t.Run("returns placeholder artwork when empty", func(t *testing.T) {
|
}
|
||||||
release := model.Release{}
|
}
|
||||||
assert.Equal(t, release.GetArtwork(), model.DEFAULT_RELEASE_ARTWORK_URL)
|
|
||||||
})
|
func Test_Release_Artwork(t *testing.T) {
|
||||||
|
want := "testartwork.png"
|
||||||
t.Run("singles", func(t *testing.T) {
|
release := Release{ Artwork: want }
|
||||||
release := model.Release{
|
|
||||||
Tracks: []*model.Track{},
|
got := release.GetArtwork()
|
||||||
}
|
if want != got {
|
||||||
|
t.Errorf(`correct value not returned when artwork is populated (want "%s", got "%s")`, want, got)
|
||||||
t.Run("false when no tracks are present", func(t *testing.T) {
|
}
|
||||||
assert.Equal(t, release.IsSingle(), false)
|
|
||||||
})
|
release = Release{}
|
||||||
|
|
||||||
release.Tracks = append(release.Tracks, &model.Track{})
|
want = "/img/default-cover-art.png"
|
||||||
t.Run("true when one track is present", func(t *testing.T) {
|
got = release.GetArtwork()
|
||||||
assert.Equal(t, release.IsSingle(), true)
|
if want != got {
|
||||||
})
|
t.Errorf(`default value not returned when artwork is empty (want "%s", got "%s")`, want, got)
|
||||||
|
}
|
||||||
release.Tracks = append(release.Tracks, &model.Track{})
|
}
|
||||||
t.Run("false when >1 tracks are present", func(t *testing.T) {
|
|
||||||
assert.Equal(t, release.IsSingle(), false)
|
func Test_Release_IsSingle(t *testing.T) {
|
||||||
})
|
release := Release{
|
||||||
})
|
Tracks: []*Track{},
|
||||||
|
}
|
||||||
t.Run("released", func(t *testing.T) {
|
|
||||||
release := model.Release {
|
if release.IsSingle() {
|
||||||
ReleaseDate: time.Now(),
|
t.Errorf("IsSingle() == true when no tracks are present")
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Run("true when release date in the past", func(t *testing.T) {
|
release.Tracks = append(release.Tracks, &Track{})
|
||||||
assert.Equal(t, release.IsReleased(), true)
|
if !release.IsSingle() {
|
||||||
})
|
t.Errorf("IsSingle() == false when one track is present")
|
||||||
|
}
|
||||||
release.ReleaseDate = time.Now().Add(time.Hour)
|
|
||||||
t.Run("false when release date in the future", func(t *testing.T) {
|
release.Tracks = append(release.Tracks, &Track{})
|
||||||
assert.Equal(t, release.IsReleased(), false)
|
if release.IsSingle() {
|
||||||
})
|
t.Errorf("IsSingle() == true when >1 tracks are present")
|
||||||
})
|
}
|
||||||
|
}
|
||||||
t.Run("printing artists", func(t *testing.T) {
|
|
||||||
artist1 := "ari melody"
|
func Test_Release_IsReleased(t *testing.T) {
|
||||||
artist2 := "aridoodle"
|
release := Release {
|
||||||
artist3 := "idk"
|
ReleaseDate: time.Now(),
|
||||||
artist4 := "guest"
|
}
|
||||||
|
|
||||||
release := model.Release{}
|
if !release.IsReleased() {
|
||||||
t.Run("prints \"Unknown Artist\" when release has no credits", func(t *testing.T) {
|
t.Errorf("IsRelease() == false when release date in the past")
|
||||||
assert.Equal(t, release.PrintArtists(false, true), "Unknown Artist")
|
}
|
||||||
})
|
|
||||||
|
release.ReleaseDate = time.Now().Add(time.Hour)
|
||||||
release.Credits = append(
|
if release.IsReleased() {
|
||||||
release.Credits,
|
t.Errorf("IsRelease() == true when release date in the future")
|
||||||
&model.Credit{ Artist: &model.Artist{ Name: artist1 }, Primary: true },
|
}
|
||||||
)
|
}
|
||||||
t.Run("prints ONLY first artist name when release has one credit", func(t *testing.T) {
|
|
||||||
assert.Equal(t, release.PrintArtists(false, true), artist1)
|
func Test_Release_PrintArtists(t *testing.T) {
|
||||||
})
|
artist1 := "ari melody"
|
||||||
|
artist2 := "aridoodle"
|
||||||
release.Credits = append(release.Credits, []*model.Credit{
|
artist3 := "idk"
|
||||||
{ Artist: &model.Artist{ Name: artist2 }, Primary: true },
|
artist4 := "guest"
|
||||||
{ Artist: &model.Artist{ Name: artist3 }, Primary: false },
|
|
||||||
{ Artist: &model.Artist{ Name: artist4 }, Primary: true },
|
release := Release {
|
||||||
}...)
|
Credits: []*Credit{
|
||||||
t.Run("can get only unique primary artist names", func(t *testing.T) {
|
{ Artist: Artist{ Name: artist1 }, Primary: true },
|
||||||
assert.Equal(
|
{ Artist: Artist{ Name: artist2 }, Primary: true },
|
||||||
t,
|
{ Artist: Artist{ Name: artist3 }, Primary: false },
|
||||||
strings.Join(release.GetUniqueArtistNames(true), " "),
|
{ Artist: Artist{ Name: artist4 }, Primary: true },
|
||||||
strings.Join([]string{ artist1, artist2, artist4 }, " "),
|
},
|
||||||
)
|
}
|
||||||
})
|
|
||||||
t.Run("can get only unique artist names", func(t *testing.T) {
|
{
|
||||||
assert.Equal(
|
want := []string{ artist1, artist2, artist4 }
|
||||||
t,
|
got := release.GetUniqueArtistNames(true)
|
||||||
strings.Join(release.GetUniqueArtistNames(false), " "),
|
if len(want) != len(got) {
|
||||||
strings.Join([]string{ artist1, artist2, artist3, artist4 }, " "),
|
t.Errorf(`len(GetUniqueArtistNames) (primary only) == %d, want %d`, len(got), len(want))
|
||||||
)
|
}
|
||||||
})
|
for i := range got {
|
||||||
|
if want[i] != got[i] {
|
||||||
t.Run("can print only primary artists, with ampersands", func(t *testing.T) {
|
t.Errorf(`GetUniqueArtistNames[%d] (primary only) == %s, want %s`, i, got[i], want[i])
|
||||||
assert.Equal(
|
}
|
||||||
t,
|
}
|
||||||
release.PrintArtists(true, true),
|
|
||||||
"ari melody, aridoodle & guest",
|
want = []string{ artist1, artist2, artist3, artist4 }
|
||||||
)
|
got = release.GetUniqueArtistNames(false)
|
||||||
})
|
if len(want) != len(got) {
|
||||||
t.Run("can print only primary artists, without ampersands", func(t *testing.T) {
|
t.Errorf(`len(GetUniqueArtistNames) == %d, want %d`, len(got), len(want))
|
||||||
assert.Equal(
|
}
|
||||||
t,
|
for i := range got {
|
||||||
release.PrintArtists(true, false),
|
if want[i] != got[i] {
|
||||||
"ari melody, aridoodle, guest",
|
t.Errorf(`GetUniqueArtistNames[%d] == %s, want %s`, i, got[i], want[i])
|
||||||
)
|
}
|
||||||
})
|
}
|
||||||
t.Run("can print all artists, with ampersands", func(t *testing.T) {
|
}
|
||||||
assert.Equal(
|
|
||||||
t,
|
{
|
||||||
release.PrintArtists(false, true),
|
want := "ari melody, aridoodle & guest"
|
||||||
"ari melody, aridoodle, idk & guest",
|
got := release.PrintArtists(true, true)
|
||||||
)
|
if want != got {
|
||||||
})
|
t.Errorf(`PrintArtists (primary only, ampersand) == "%s", want "%s"`, want, got)
|
||||||
t.Run("can print all artists, without ampersands", func(t *testing.T) {
|
}
|
||||||
assert.Equal(
|
|
||||||
t,
|
want = "ari melody, aridoodle, guest"
|
||||||
release.PrintArtists(false, false),
|
got = release.PrintArtists(true, false)
|
||||||
"ari melody, aridoodle, idk, guest",
|
if want != got {
|
||||||
)
|
t.Errorf(`PrintArtists (primary only) == "%s", want "%s"`, want, got)
|
||||||
})
|
}
|
||||||
})
|
|
||||||
|
want = "ari melody, aridoodle, idk & guest"
|
||||||
t.Run("validating release types", func(t *testing.T) {
|
got = release.PrintArtists(false, true)
|
||||||
t.Run("single", func(t *testing.T) {
|
if want != got {
|
||||||
releaseType, ok := model.ValidReleaseType("single")
|
t.Errorf(`PrintArtists (all, ampersand) == "%s", want "%s"`, want, got)
|
||||||
assert.Equal(t, ok, true)
|
}
|
||||||
assert.Equal(t, releaseType, model.Single)
|
|
||||||
})
|
want = "ari melody, aridoodle, idk, guest"
|
||||||
t.Run("album", func(t *testing.T) {
|
got = release.PrintArtists(false, false)
|
||||||
releaseType, ok := model.ValidReleaseType("album")
|
if want != got {
|
||||||
assert.Equal(t, ok, true)
|
t.Errorf(`PrintArtists (all) == "%s", want "%s"`, want, got)
|
||||||
assert.Equal(t, releaseType, model.Album)
|
}
|
||||||
})
|
}
|
||||||
t.Run("EP", func(t *testing.T) {
|
|
||||||
releaseType, ok := model.ValidReleaseType("EP")
|
|
||||||
assert.Equal(t, ok, true)
|
|
||||||
assert.Equal(t, releaseType, model.EP)
|
|
||||||
})
|
|
||||||
t.Run("compilation", func(t *testing.T) {
|
|
||||||
releaseType, ok := model.ValidReleaseType("compilation")
|
|
||||||
assert.Equal(t, ok, true)
|
|
||||||
assert.Equal(t, releaseType, model.Compilation)
|
|
||||||
})
|
|
||||||
t.Run("upcoming", func(t *testing.T) {
|
|
||||||
releaseType, ok := model.ValidReleaseType("upcoming")
|
|
||||||
assert.Equal(t, ok, true)
|
|
||||||
assert.Equal(t, releaseType, model.Upcoming)
|
|
||||||
})
|
|
||||||
t.Run("invalid", func(t *testing.T) {
|
|
||||||
releaseType, ok := model.ValidReleaseType("invalid")
|
|
||||||
assert.Equal(t, ok, false)
|
|
||||||
assert.Equal(t, string(releaseType), "")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,3 +24,8 @@ func (track Track) GetDescriptionHTML() template.HTML {
|
||||||
func (track Track) GetLyricsHTML() template.HTML {
|
func (track Track) GetLyricsHTML() template.HTML {
|
||||||
return template.HTML(strings.ReplaceAll(track.Lyrics, "\n", "<br>"))
|
return template.HTML(strings.ReplaceAll(track.Lyrics, "\n", "<br>"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// this function is stupid and i hate that i need it
|
||||||
|
func (track Track) Add(a int, b int) int {
|
||||||
|
return a + b
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
package model_test
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/model"
|
"testing"
|
||||||
"testing"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_Track_DescriptionHTML(t *testing.T) {
|
func Test_Track_DescriptionHTML(t *testing.T) {
|
||||||
track := model.Track{
|
track := Track{
|
||||||
Description: "this is\na test\n<strong>description!</strong>",
|
Description: "this is\na test\n<strong>description!</strong>",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -20,7 +19,7 @@ func Test_Track_DescriptionHTML(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func Test_Track_LyricsHTML(t *testing.T) {
|
func Test_Track_LyricsHTML(t *testing.T) {
|
||||||
track := model.Track{
|
track := Track{
|
||||||
Lyrics: "these are\ntest\n<strong>lyrics!</strong>",
|
Lyrics: "these are\ntest\n<strong>lyrics!</strong>",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -32,3 +31,13 @@ func Test_Track_LyricsHTML(t *testing.T) {
|
||||||
t.Errorf(`track lyrics incorrectly formatted (want "%s", got "%s")`, want, got)
|
t.Errorf(`track lyrics incorrectly formatted (want "%s", got "%s")`, want, got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Track_Add(t *testing.T) {
|
||||||
|
track := Track{}
|
||||||
|
|
||||||
|
want := 4
|
||||||
|
got := track.Add(2, 2)
|
||||||
|
if want != got {
|
||||||
|
t.Errorf(`somehow, we screwed up addition. (want %d, got %d)`, want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
package twitch
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -7,17 +7,17 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type (
|
type (
|
||||||
OAuthToken struct {
|
TwitchOAuthToken struct {
|
||||||
AccessToken string
|
AccessToken string
|
||||||
ExpiresAt time.Time
|
ExpiresAt time.Time
|
||||||
TokenType string
|
TokenType string
|
||||||
}
|
}
|
||||||
|
|
||||||
State struct {
|
TwitchState struct {
|
||||||
Token *OAuthToken
|
Token *TwitchOAuthToken
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamInfo struct {
|
TwitchStreamInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
UserLogin string `json:"user_login"`
|
UserLogin string `json:"user_login"`
|
||||||
|
|
@ -36,7 +36,7 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func (info *StreamInfo) Thumbnail(width int, height int) string {
|
func (info *TwitchStreamInfo) Thumbnail(width int, height int) string {
|
||||||
res := strings.Replace(info.ThumbnailURL, "{width}", fmt.Sprintf("%d", width), 1)
|
res := strings.Replace(info.ThumbnailURL, "{width}", fmt.Sprintf("%d", width), 1)
|
||||||
res = strings.Replace(res, "{height}", fmt.Sprintf("%d", height), 1)
|
res = strings.Replace(res, "{height}", fmt.Sprintf("%d", height), 1)
|
||||||
return res
|
return res
|
||||||
|
|
@ -153,7 +153,7 @@ header ul li a:hover {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
border-bottom: 1px solid #888;
|
border-bottom: 1px solid #888;
|
||||||
background-color: var(--background);
|
background: var(--background);
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: var(--background);
|
background: var(--background);
|
||||||
color: var(--on-background);
|
color: var(--on-background);
|
||||||
font-family: "Monaspace Argon", monospace;
|
font-family: "Monaspace Argon", monospace;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
|
@ -150,7 +150,7 @@ a#backtotop:hover {
|
||||||
@keyframes list-item-fadein {
|
@keyframes list-item-fadein {
|
||||||
from {
|
from {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background-color: var(--links);
|
background: var(--links);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ header {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
filter: blur(25px) saturate(25%) brightness(0.5);
|
filter: blur(25px) saturate(25%) brightness(0.5);
|
||||||
-webkit-filter: blur(25px) saturate(25%) brightness(0.5);
|
-webkit-filter: blur(25px) saturate(25%) brightness(0.5);;
|
||||||
animation: background-init .5s forwards,background-loop 30s ease-in-out infinite
|
animation: background-init .5s forwards,background-loop 30s ease-in-out infinite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
package account
|
|
||||||
|
|
||||||
import "arimelody-web/model"
|
|
||||||
|
|
||||||
type AccountRepository interface {
|
|
||||||
GetAll() ([]*model.Account, error)
|
|
||||||
GetCount() (int, error)
|
|
||||||
// Fetches an account by ID, returning an error if one was encountered.
|
|
||||||
// If the account does not exist, both response fields are nil.
|
|
||||||
GetByID(id string) (*model.Account, error)
|
|
||||||
GetByUsername(username string) (*model.Account, error)
|
|
||||||
GetByEmail(email string) (*model.Account, error)
|
|
||||||
|
|
||||||
// Pulled this function: Cross-cutting concerns between accounts and sessions.
|
|
||||||
// Instead, fetch account ID from session and use GetByID()
|
|
||||||
// GetBySession(sessionToken string) (*model.Account, error)
|
|
||||||
|
|
||||||
// Create an account, returning the new account ID.
|
|
||||||
Create(username string, password string, email *string, avatarURL *string) (string, error)
|
|
||||||
|
|
||||||
// Deprecated in favour of more specialised Update* and Remove* functions.
|
|
||||||
Update(id string, username string, password string, email *string, avatarUrl *string) error
|
|
||||||
UpdateUsername(id string, username string) error
|
|
||||||
UpdatePassword(id string, password string) error
|
|
||||||
UpdateEmail(id string, email string) error
|
|
||||||
RemoveEmail(id string) error
|
|
||||||
UpdateAvatarURL(id string, avatarURL string) error
|
|
||||||
RemoveAvatar(id string) error
|
|
||||||
|
|
||||||
// Increment the number of account login failure attempts,
|
|
||||||
// returning the current fail count.
|
|
||||||
IncrementFails(id string) (int, error)
|
|
||||||
ResetFails(id string) error
|
|
||||||
SetLocked(id string, lock bool) error
|
|
||||||
|
|
||||||
Delete(id string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
@ -1,188 +0,0 @@
|
||||||
package account
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
"database/sql"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AccountRepositoryMemory struct {
|
|
||||||
accounts []*model.Account
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ AccountRepository = new(AccountRepositoryMemory)
|
|
||||||
|
|
||||||
func NewAccountRepositoryMemory(accounts []*model.Account) *AccountRepositoryMemory {
|
|
||||||
return &AccountRepositoryMemory{ accounts: accounts }
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryMemory) GetAll() ([]*model.Account, error) {
|
|
||||||
return repo.accounts, nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) GetCount() (int, error) {
|
|
||||||
return len(repo.accounts), nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) GetByID(id string) (*model.Account, error) {
|
|
||||||
index := slices.IndexFunc(repo.accounts, func(account *model.Account) bool {
|
|
||||||
return account.ID == id
|
|
||||||
})
|
|
||||||
if index == -1 { return nil, nil }
|
|
||||||
return repo.accounts[index], nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) GetByUsername(username string) (*model.Account, error) {
|
|
||||||
for _, account := range repo.accounts {
|
|
||||||
if account.Username == username { return account, nil }
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) GetByEmail(email string) (*model.Account, error) {
|
|
||||||
for _, account := range repo.accounts {
|
|
||||||
if account.Email.Valid && account.Email.String == email {
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an account, returning the new account ID.
|
|
||||||
func (repo *AccountRepositoryMemory) Create(username string, password string, email *string, avatarURL *string) (string, error) {
|
|
||||||
if account, err := repo.GetByUsername(username); err != nil {
|
|
||||||
return "", errors.NewNotExistError("Failed to fetch other acccounts by username")
|
|
||||||
} else if account != nil {
|
|
||||||
return "", errors.NewNotExistError("Account with this username already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
emailRef := ""
|
|
||||||
if email != nil { emailRef = *email }
|
|
||||||
avatarURLRef := ""
|
|
||||||
if avatarURL != nil { avatarURLRef = *avatarURL }
|
|
||||||
|
|
||||||
id := strconv.Itoa(len(repo.accounts))
|
|
||||||
|
|
||||||
repo.accounts = append(repo.accounts, &model.Account{
|
|
||||||
ID: id,
|
|
||||||
Username: username,
|
|
||||||
Password: password,
|
|
||||||
Email: sql.NullString{ String: emailRef, Valid: email != nil },
|
|
||||||
AvatarURL: sql.NullString{ String: avatarURLRef, Valid: avatarURL != nil },
|
|
||||||
})
|
|
||||||
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Intended for large profile updates. For smaller adjusments,
|
|
||||||
// more specialised Update* and Remove* functions should be used.
|
|
||||||
func (repo *AccountRepositoryMemory) Update(id string, username string, password string, email *string, avatarUrl *string) error {
|
|
||||||
if account, err := repo.GetByUsername(username); err != nil {
|
|
||||||
return errors.NewNotExistError("Failed to fetch other acccounts by username")
|
|
||||||
} else if account != nil && account.ID != id {
|
|
||||||
return errors.NewNotExistError("Account with this username already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
|
|
||||||
account.Username = username
|
|
||||||
account.Password = password
|
|
||||||
account.Email.Valid = email != nil
|
|
||||||
if account.Email.Valid { account.Email.String = *email }
|
|
||||||
account.AvatarURL.Valid = avatarUrl != nil
|
|
||||||
if account.AvatarURL.Valid { account.AvatarURL.String = *avatarUrl }
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) UpdateUsername(id string, username string) error {
|
|
||||||
if account, err := repo.GetByUsername(username); err != nil {
|
|
||||||
return errors.NewNotExistError("Failed to fetch other acccounts by username")
|
|
||||||
} else if account != nil && account.ID != id {
|
|
||||||
return errors.NewNotExistError("Account with this username already exists")
|
|
||||||
}
|
|
||||||
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if account == nil { return errors.NewNotExistError("Account does not exist") }
|
|
||||||
|
|
||||||
account.Username = username
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) UpdatePassword(id string, password string) error {
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if account == nil { return errors.NewNotExistError("Account does not exist") }
|
|
||||||
|
|
||||||
account.Password = password
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) UpdateEmail(id string, email string) error {
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if account == nil { return errors.NewNotExistError("Account does not exist") }
|
|
||||||
|
|
||||||
account.Email.Valid = true
|
|
||||||
account.Email.String = email
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) RemoveEmail(id string) error {
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if account == nil { return errors.NewNotExistError("Account does not exist") }
|
|
||||||
|
|
||||||
account.Email.Valid = false
|
|
||||||
account.Email.String = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) UpdateAvatarURL(id string, avatarURL string) error {
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if account == nil { return errors.NewNotExistError("Account does not exist") }
|
|
||||||
|
|
||||||
account.AvatarURL.Valid = true
|
|
||||||
account.AvatarURL.String = avatarURL
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) RemoveAvatar(id string) error {
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if account == nil { return errors.NewNotExistError("Account does not exist") }
|
|
||||||
|
|
||||||
account.AvatarURL.Valid = false
|
|
||||||
account.AvatarURL.String = ""
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment the number of account login failure attempts,
|
|
||||||
// returning the current fail count.
|
|
||||||
func (repo *AccountRepositoryMemory) IncrementFails(id string) (int, error) {
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return 0, err }
|
|
||||||
account.FailAttempts += 1
|
|
||||||
return account.FailAttempts, nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) ResetFails(id string) error {
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
account.FailAttempts = 0
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryMemory) SetLocked(id string, locked bool) error {
|
|
||||||
account, err := repo.GetByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
account.Locked = locked
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryMemory) Delete(id string) (string, error) {
|
|
||||||
var deletedID string
|
|
||||||
newAccounts := []*model.Account{}
|
|
||||||
for _, account := range repo.accounts {
|
|
||||||
if account.ID == id {
|
|
||||||
deletedID = id
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newAccounts = append(newAccounts, account)
|
|
||||||
}
|
|
||||||
repo.accounts = newAccounts
|
|
||||||
return deletedID, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,182 +0,0 @@
|
||||||
package account
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AccountRepositoryPostgres struct {
|
|
||||||
db *sqlx.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ AccountRepository = new(AccountRepositoryPostgres)
|
|
||||||
|
|
||||||
func NewAccountRepositoryPostgres(db *sqlx.DB) *AccountRepositoryPostgres {
|
|
||||||
return &AccountRepositoryPostgres{ db: db }
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) GetAll() ([]*model.Account, error) {
|
|
||||||
var accounts = []*model.Account{}
|
|
||||||
|
|
||||||
err := repo.db.Select(&accounts, "SELECT * FROM account ORDER BY created_at ASC")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return accounts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) GetCount() (int, error) {
|
|
||||||
accountsCount := 0
|
|
||||||
err := repo.db.Get(&accountsCount, "SELECT count(*) FROM account")
|
|
||||||
return accountsCount, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) GetByID(id string) (*model.Account, error) {
|
|
||||||
var account = model.Account{}
|
|
||||||
|
|
||||||
err := repo.db.Get(&account, "SELECT * FROM account WHERE id=$1", id)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) GetByUsername(username string) (*model.Account, error) {
|
|
||||||
var account = model.Account{}
|
|
||||||
|
|
||||||
err := repo.db.Get(&account, "SELECT * FROM account WHERE username=$1", username)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) GetByEmail(email string) (*model.Account, error) {
|
|
||||||
var account = model.Account{}
|
|
||||||
|
|
||||||
err := repo.db.Get(&account, "SELECT * FROM account WHERE email=$1", email)
|
|
||||||
if err != nil {
|
|
||||||
if strings.Contains(err.Error(), "no rows") {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) Create(
|
|
||||||
username string,
|
|
||||||
password string,
|
|
||||||
email *string,
|
|
||||||
avatarURL *string,
|
|
||||||
) (string, error) {
|
|
||||||
var id string
|
|
||||||
|
|
||||||
err := repo.db.Get(
|
|
||||||
&id,
|
|
||||||
"INSERT INTO account (username, password, email, avatar_url) " +
|
|
||||||
"VALUES ($1, $2, $3, $4) " +
|
|
||||||
"RETURNING id",
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
email,
|
|
||||||
avatarURL,
|
|
||||||
)
|
|
||||||
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) Update(
|
|
||||||
id string,
|
|
||||||
username string,
|
|
||||||
password string,
|
|
||||||
email *string,
|
|
||||||
avatarURL *string,
|
|
||||||
) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE account " +
|
|
||||||
"SET username=$2,password=$3,email=$4,avatar_url=$5 " +
|
|
||||||
"WHERE id=$1",
|
|
||||||
id,
|
|
||||||
username,
|
|
||||||
password,
|
|
||||||
email,
|
|
||||||
avatarURL,
|
|
||||||
)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) UpdateUsername(id string, username string) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE account SET username=$2 WHERE id=$1",
|
|
||||||
id, username,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryPostgres) UpdatePassword(id string, password string) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE account SET password=$2 WHERE id=$1",
|
|
||||||
id, password,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryPostgres) UpdateEmail(id string, email string) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE account SET email=$2 WHERE id=$1",
|
|
||||||
id, email,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryPostgres) RemoveEmail(id string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE account SET email=NULL WHERE id=$1", id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryPostgres) UpdateAvatarURL(id string, avatarURL string) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE account SET avatar_url=$2 WHERE id=$1",
|
|
||||||
id, avatarURL,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *AccountRepositoryPostgres) RemoveAvatar(id string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE account SET avatar_url=NULL WHERE id=$1", id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment the number of account login failure attempts,
|
|
||||||
// returning the current fail count.
|
|
||||||
func (repo *AccountRepositoryPostgres) IncrementFails(id string) (int, error) {
|
|
||||||
failAttempts := 0
|
|
||||||
err := repo.db.Get(&failAttempts, "UPDATE account SET fail_attempts = fail_attempts + 1 WHERE id=$1 RETURNING fail_attempts", id)
|
|
||||||
return failAttempts, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) ResetFails(id string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE account SET fail_attempts = 0 WHERE id=$1", id)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) SetLocked(id string, locked bool) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE account SET locked = $2 WHERE id=$1", id, locked)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *AccountRepositoryPostgres) Delete(id string) (string, error) {
|
|
||||||
var deletedID string
|
|
||||||
err := repo.db.Get(&deletedID, "DELETE FROM account WHERE id=$1", id)
|
|
||||||
return deletedID, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package log
|
|
||||||
|
|
||||||
import "arimelody-web/model"
|
|
||||||
|
|
||||||
type LogRepository interface {
|
|
||||||
Create(logLevel model.LogLevel, logType string, content string) error
|
|
||||||
Get(id string) (*model.Log, error)
|
|
||||||
Search(levelFilters []model.LogLevel, typeFilters []string, content string, limit int, offset int) ([]*model.Log, error)
|
|
||||||
}
|
|
||||||
|
|
@ -1,108 +0,0 @@
|
||||||
package log
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
_ "github.com/lib/pq"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LogRepositoryPostgres struct {
|
|
||||||
db *sqlx.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ LogRepository = new(LogRepositoryPostgres)
|
|
||||||
|
|
||||||
func NewLogRepositoryPostgres(db *sqlx.DB) *LogRepositoryPostgres {
|
|
||||||
return &LogRepositoryPostgres{ db: db }
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *LogRepositoryPostgres) Create(logLevel model.LogLevel, logType string, content string) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"INSERT INTO auditlog (level, type, content) VALUES ($1,$2,$3)",
|
|
||||||
logLevel,
|
|
||||||
logType,
|
|
||||||
content,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *LogRepositoryPostgres) Get(id string) (*model.Log, error) {
|
|
||||||
log := model.Log{}
|
|
||||||
err := repo.db.Get(&log, "SELECT * FROM auditlog WHERE id=$1", id)
|
|
||||||
return &log, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *LogRepositoryPostgres) Search(
|
|
||||||
levelFilters []model.LogLevel,
|
|
||||||
typeFilters []string,
|
|
||||||
content string,
|
|
||||||
limit int,
|
|
||||||
offset int,
|
|
||||||
) ([]*model.Log, error) {
|
|
||||||
logs := []*model.Log{}
|
|
||||||
|
|
||||||
params := []any{ limit, offset }
|
|
||||||
conditions := ""
|
|
||||||
|
|
||||||
if len(content) > 0 {
|
|
||||||
content = "%" + content + "%"
|
|
||||||
conditions += " WHERE content LIKE $3"
|
|
||||||
params = append(params, content)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(levelFilters) > 0 {
|
|
||||||
if len(conditions) > 0 {
|
|
||||||
conditions += " AND level IN ("
|
|
||||||
} else {
|
|
||||||
conditions += " WHERE level IN ("
|
|
||||||
}
|
|
||||||
for i := range levelFilters {
|
|
||||||
conditions += fmt.Sprintf("$%d", len(params) + 1)
|
|
||||||
if i < len(levelFilters) - 1 {
|
|
||||||
conditions += ","
|
|
||||||
}
|
|
||||||
params = append(params, levelFilters[i])
|
|
||||||
}
|
|
||||||
conditions += ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(typeFilters) > 0 {
|
|
||||||
if len(conditions) > 0 {
|
|
||||||
conditions += " AND type IN ("
|
|
||||||
} else {
|
|
||||||
conditions += " WHERE type IN ("
|
|
||||||
}
|
|
||||||
for i := range typeFilters {
|
|
||||||
conditions += fmt.Sprintf("$%d", len(params) + 1)
|
|
||||||
if i < len(typeFilters) - 1 {
|
|
||||||
conditions += ","
|
|
||||||
}
|
|
||||||
params = append(params, typeFilters[i])
|
|
||||||
}
|
|
||||||
conditions += ")"
|
|
||||||
}
|
|
||||||
|
|
||||||
query := fmt.Sprintf(
|
|
||||||
"SELECT * FROM auditlog%s ORDER BY created_at DESC LIMIT $1 OFFSET $2",
|
|
||||||
conditions,
|
|
||||||
)
|
|
||||||
|
|
||||||
/*
|
|
||||||
fmt.Printf("%s (", query)
|
|
||||||
for i, param := range params {
|
|
||||||
fmt.Print(param)
|
|
||||||
if i < len(params) - 1 {
|
|
||||||
fmt.Print(", ")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fmt.Print(")\n")
|
|
||||||
*/
|
|
||||||
|
|
||||||
err := repo.db.Select(&logs, query, params...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return logs, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
"slices"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetAllArtists() ([]*model.Artist, error) {
|
|
||||||
return repo.artists, nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) GetArtistCount() (int, error) {
|
|
||||||
return len(repo.artists), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetArtistByID(id string) (*model.Artist, error) {
|
|
||||||
index := slices.IndexFunc(repo.artists, func(artist *model.Artist) bool {
|
|
||||||
return artist.ID == id
|
|
||||||
})
|
|
||||||
if index == -1 { return nil, nil }
|
|
||||||
return repo.artists[index], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error) {
|
|
||||||
release, err := repo.GetReleaseByID(releaseID)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
artists, err := repo.GetAllArtists()
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
|
|
||||||
artistsNotOnRelease := []*model.Artist{}
|
|
||||||
for _, artist := range artists {
|
|
||||||
if !slices.ContainsFunc(release.Credits, func(credit *model.Credit) bool {
|
|
||||||
return credit.Artist.ID == artist.ID
|
|
||||||
}) {
|
|
||||||
artistsNotOnRelease = append(artistsNotOnRelease, artist)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return artistsNotOnRelease, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error) {
|
|
||||||
releases, err := repo.GetAllReleases(!showHidden, 0)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
|
|
||||||
credits := []*model.Credit{}
|
|
||||||
for _, release := range releases {
|
|
||||||
credits = append(credits, slices.DeleteFunc(
|
|
||||||
release.Credits,
|
|
||||||
func(credit *model.Credit) bool {
|
|
||||||
return credit.Artist.ID != artistID
|
|
||||||
},
|
|
||||||
)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
return credits, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) CreateArtist(
|
|
||||||
id string,
|
|
||||||
name string,
|
|
||||||
website string,
|
|
||||||
avatar string,
|
|
||||||
) error {
|
|
||||||
if artist, err := repo.GetArtistByID(id); err != nil {
|
|
||||||
if !errors.IsNotExistError(err) { return err }
|
|
||||||
} else {
|
|
||||||
if artist != nil { return errors.NewValidationError("Artist with this ID already exists") }
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.artists = append(repo.artists, &model.Artist{
|
|
||||||
ID: id,
|
|
||||||
Name: name,
|
|
||||||
Website: website,
|
|
||||||
Avatar: avatar,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateArtist(artist *model.Artist) error {
|
|
||||||
repoArtist, err := repo.GetArtistByID(artist.ID)
|
|
||||||
if err != nil { return err }
|
|
||||||
if repoArtist == nil { return errors.NewNotExistError("Artist does not exist") }
|
|
||||||
|
|
||||||
repoArtist.Name = artist.Name
|
|
||||||
repoArtist.Website = artist.Website
|
|
||||||
repoArtist.Avatar = artist.Avatar
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateArtistID(oldID string, newID string) error {
|
|
||||||
artist, err := repo.GetArtistByID(oldID)
|
|
||||||
if err != nil { return err }
|
|
||||||
if artist == nil { return errors.NewNotExistError("Artist does not exist") }
|
|
||||||
|
|
||||||
artist.ID = newID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateArtistName(id string, name string) error {
|
|
||||||
artist, err := repo.GetArtistByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if artist == nil { return errors.NewNotExistError("Artist does not exist") }
|
|
||||||
|
|
||||||
artist.Name = name
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateArtistWebsite(id string, website string) error {
|
|
||||||
artist, err := repo.GetArtistByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if artist == nil { return errors.NewNotExistError("Artist does not exist") }
|
|
||||||
|
|
||||||
artist.Website = website
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateArtistAvatar(id string, avatar string) error {
|
|
||||||
artist, err := repo.GetArtistByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if artist == nil { return errors.NewNotExistError("Artist does not exist") }
|
|
||||||
|
|
||||||
artist.Avatar = avatar
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) DeleteArtist(id string) (string, error) {
|
|
||||||
var deletedID string
|
|
||||||
newArtists := []*model.Artist{}
|
|
||||||
for _, artist := range repo.artists {
|
|
||||||
if artist.ID == id {
|
|
||||||
deletedID = id
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newArtists = append(newArtists, artist)
|
|
||||||
}
|
|
||||||
repo.artists = newArtists
|
|
||||||
return deletedID, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,110 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MusicRepository interface {
|
|
||||||
// artists
|
|
||||||
|
|
||||||
GetAllArtists() ([]*model.Artist, error)
|
|
||||||
GetArtistCount() (int, error)
|
|
||||||
// Fetches an artist by ID, returning an error if one was encountered.
|
|
||||||
// If the artist does not exist, both response fields are nil.
|
|
||||||
GetArtistByID(id string) (*model.Artist, error)
|
|
||||||
GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error)
|
|
||||||
GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error)
|
|
||||||
|
|
||||||
CreateArtist(id string, name string, website string, avatar string) error
|
|
||||||
|
|
||||||
UpdateArtist(artist *model.Artist) error
|
|
||||||
UpdateArtistID(oldID string, newID string) error
|
|
||||||
UpdateArtistName(id string, name string) error
|
|
||||||
UpdateArtistWebsite(id string, website string) error
|
|
||||||
UpdateArtistAvatar(id string, avatar string) error
|
|
||||||
|
|
||||||
DeleteArtist(id string) (string, error)
|
|
||||||
|
|
||||||
// releases
|
|
||||||
|
|
||||||
// Fetch all releases.
|
|
||||||
// Filters to visible releases if `onlyVisible = true`.
|
|
||||||
// If `limit > 0`, limits the number of results.
|
|
||||||
GetAllReleases(onlyVisible bool, limit int) ([]*model.Release, error)
|
|
||||||
GetReleaseCount(onlyVisible bool) (int, error)
|
|
||||||
GetReleaseByID(id string) (*model.Release, error)
|
|
||||||
|
|
||||||
GetReleaseTracks(id string) ([]*model.Track, error)
|
|
||||||
GetReleaseCredits(id string) ([]*model.Credit, error)
|
|
||||||
GetReleaseLinks(id string) ([]*model.Link, error)
|
|
||||||
|
|
||||||
CreateRelease(id string, title string, releaseType model.ReleaseType, releaseDate time.Time, artworkURL string) error
|
|
||||||
|
|
||||||
UpdateRelease(release *model.Release) error
|
|
||||||
UpdateReleaseID(oldID string, newID string) error
|
|
||||||
UpdateReleaseVisibility(id string, visible bool) error
|
|
||||||
UpdateReleaseTitle(id string, title string) error
|
|
||||||
UpdateReleaseDescription(id string, description string) error
|
|
||||||
UpdateReleaseType(id string, releaseType model.ReleaseType) error
|
|
||||||
UpdateReleaseDate(id string, releaseDate time.Time) error
|
|
||||||
UpdateReleaseArtwork(id string, artwork string) error
|
|
||||||
UpdateReleaseBuyInfo(id string, buyName string, buyLink string) error
|
|
||||||
UpdateReleaseCopyright(id string, copyright string, url string) error
|
|
||||||
UpdateReleaseTracks(id string, newTrackIDs []string) error
|
|
||||||
UpdateReleaseCredits(id string, newCredits []*model.Credit) error
|
|
||||||
UpdateReleaseLinks(id string, newLinks []*model.Link) error
|
|
||||||
|
|
||||||
DeleteRelease(id string) (string, error)
|
|
||||||
|
|
||||||
// tracks
|
|
||||||
|
|
||||||
GetAllTracks() ([]*model.Track, error)
|
|
||||||
GetTrackCount() (int, error)
|
|
||||||
GetTrackByID(id string) (*model.Track, error)
|
|
||||||
GetOrphanTracks() ([]*model.Track, error)
|
|
||||||
GetTracksNotOnRelease(releaseID string) ([]*model.Track, error)
|
|
||||||
GetTrackReleases(trackID string) ([]*model.Release, error)
|
|
||||||
|
|
||||||
CreateTrack(title string, description string, lyrics string, previewURL string) (string, error)
|
|
||||||
|
|
||||||
UpdateTrack(track *model.Track) error
|
|
||||||
UpdateTrackTitle(id string, title string) error
|
|
||||||
UpdateTrackDescription(id string, description string) error
|
|
||||||
UpdateTrackLyrics(id string, lyrics string) error
|
|
||||||
UpdateTrackPreviewURL(id string, previewURL string) error
|
|
||||||
|
|
||||||
DeleteTrack(id string) (string, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
|
||||||
MusicRepositoryPostgres struct {
|
|
||||||
db *sqlx.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
MusicRepositoryMemory struct {
|
|
||||||
artists []*model.Artist
|
|
||||||
releases []*model.Release
|
|
||||||
tracks []*model.Track
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ MusicRepository = new(MusicRepositoryPostgres)
|
|
||||||
func NewMusicRepositoryPostgres(db *sqlx.DB) *MusicRepositoryPostgres {
|
|
||||||
return &MusicRepositoryPostgres{ db: db }
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ MusicRepository = new(MusicRepositoryMemory)
|
|
||||||
func NewMusicRepositoryMemory(
|
|
||||||
artists []*model.Artist,
|
|
||||||
releases []*model.Release,
|
|
||||||
tracks []*model.Track,
|
|
||||||
) *MusicRepositoryMemory {
|
|
||||||
return &MusicRepositoryMemory{
|
|
||||||
artists: artists,
|
|
||||||
releases: releases,
|
|
||||||
tracks: tracks,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,228 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
"fmt"
|
|
||||||
"slices"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetAllReleases(onlyVisible bool, limit int) ([]*model.Release, error) {
|
|
||||||
releases := []*model.Release{}
|
|
||||||
for _, release := range repo.releases {
|
|
||||||
if !onlyVisible || release.Visible {
|
|
||||||
releases = append(releases, release)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return releases, nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) GetReleaseCount(onlyVisible bool) (int, error) {
|
|
||||||
releaseCount := 0
|
|
||||||
for _, release := range repo.releases {
|
|
||||||
if !onlyVisible || release.Visible {
|
|
||||||
releaseCount++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return releaseCount, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetReleaseByID(id string) (*model.Release, error) {
|
|
||||||
index := slices.IndexFunc(repo.releases, func(release *model.Release) bool {
|
|
||||||
return release.ID == id
|
|
||||||
})
|
|
||||||
if index == -1 { return nil, nil }
|
|
||||||
return repo.releases[index], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetReleaseTracks(id string) ([]*model.Track, error) {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
return release.Tracks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetReleaseCredits(id string) ([]*model.Credit, error) {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
return release.Credits, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetReleaseLinks(id string) ([]*model.Link, error) {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
return release.Links, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) CreateRelease(
|
|
||||||
id string,
|
|
||||||
title string,
|
|
||||||
releaseType model.ReleaseType,
|
|
||||||
releaseDate time.Time,
|
|
||||||
artworkURL string,
|
|
||||||
) error {
|
|
||||||
if release, err := repo.GetReleaseByID(id); err != nil {
|
|
||||||
if !errors.IsNotExistError(err) { return err }
|
|
||||||
} else {
|
|
||||||
if release != nil { return errors.NewValidationError("Release with this ID already exists") }
|
|
||||||
}
|
|
||||||
|
|
||||||
repo.releases = append(repo.releases, &model.Release{
|
|
||||||
ID: id,
|
|
||||||
Title: title,
|
|
||||||
ReleaseType: releaseType,
|
|
||||||
ReleaseDate: releaseDate,
|
|
||||||
Artwork: artworkURL,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateRelease(release *model.Release) error {
|
|
||||||
repoRelease, err := repo.GetReleaseByID(release.ID)
|
|
||||||
if err != nil { return err }
|
|
||||||
if repoRelease == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
|
|
||||||
repoRelease.Visible = release.Visible
|
|
||||||
repoRelease.Title = release.Title
|
|
||||||
repoRelease.Description = release.Description
|
|
||||||
repoRelease.ReleaseType = release.ReleaseType
|
|
||||||
repoRelease.ReleaseDate = release.ReleaseDate
|
|
||||||
repoRelease.Artwork = release.Artwork
|
|
||||||
repoRelease.Buyname = release.Buyname
|
|
||||||
repoRelease.Buylink = release.Buylink
|
|
||||||
repoRelease.Copyright = release.Copyright
|
|
||||||
repoRelease.CopyrightURL = release.CopyrightURL
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseID(oldID string, newID string) error {
|
|
||||||
release, err := repo.GetReleaseByID(oldID)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.ID = newID
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseVisibility(id string, visible bool) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.Visible = visible
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseTitle(id string, title string) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.Title = title
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseDescription(id string, description string) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.Description = description
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseType(id string, releaseType model.ReleaseType) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.ReleaseType = releaseType
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseDate(id string, releaseDate time.Time) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.ReleaseDate = releaseDate
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseArtwork(id string, artwork string) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.Artwork = artwork
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseBuyInfo(id string, buyName string, buyLink string) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.Buyname = buyName
|
|
||||||
release.Buylink = buyLink
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseCopyright(id string, copyright string, url string) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.Copyright = copyright
|
|
||||||
release.CopyrightURL = url
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseTracks(id string, newTrackIDs []string) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
|
|
||||||
tracks := []*model.Track{}
|
|
||||||
for _, trackID := range newTrackIDs {
|
|
||||||
track, err := repo.GetTrackByID(trackID)
|
|
||||||
if err != nil {
|
|
||||||
if errors.IsNotExistError(err) {
|
|
||||||
return errors.NewNotExistError(fmt.Sprintf("Track %s does not exist", trackID))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
tracks = append(tracks, track)
|
|
||||||
}
|
|
||||||
release.Tracks = tracks
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseCredits(id string, newCredits []*model.Credit) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
|
|
||||||
for _, credit := range newCredits {
|
|
||||||
if credit.Artist == nil { return errors.NewValidationError("Credit artist cannot be empty") }
|
|
||||||
if len(credit.Artist.ID) == 0 { return errors.NewValidationError("Credit artist ID cannot be empty") }
|
|
||||||
if artist, err := repo.GetArtistByID(credit.Artist.ID); err != nil {
|
|
||||||
return err
|
|
||||||
} else if artist == nil {
|
|
||||||
return errors.NewNotExistError(fmt.Sprintf("Artist '%s' does not exist", credit.Artist.ID))
|
|
||||||
} else {
|
|
||||||
credit.Release = release
|
|
||||||
credit.Artist = artist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
release.Credits = newCredits
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateReleaseLinks(id string, newLinks []*model.Link) error {
|
|
||||||
release, err := repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if release == nil { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
release.Links = newLinks
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) DeleteRelease(id string) (string, error) {
|
|
||||||
var deletedID string
|
|
||||||
newReleases := []*model.Release{}
|
|
||||||
for _, release := range repo.releases {
|
|
||||||
if release.ID == id {
|
|
||||||
deletedID = id
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newReleases = append(newReleases, release)
|
|
||||||
}
|
|
||||||
repo.releases = newReleases
|
|
||||||
return deletedID, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,303 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetAllReleases(onlyVisible bool, limit int) ([]*model.Release, error) {
|
|
||||||
var releases = []*model.Release{}
|
|
||||||
|
|
||||||
query := "SELECT * FROM musicrelease"
|
|
||||||
if onlyVisible {
|
|
||||||
query += " WHERE visible=true"
|
|
||||||
}
|
|
||||||
query += " ORDER BY release_date DESC"
|
|
||||||
var err error
|
|
||||||
if limit > 0 {
|
|
||||||
err = repo.db.Select(&releases, query + " LIMIT $1", limit)
|
|
||||||
} else {
|
|
||||||
err = repo.db.Select(&releases, query)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return releases, nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) GetReleaseCount(onlyVisible bool) (int, error) {
|
|
||||||
query := "SELECT count(*) FROM musicrelease"
|
|
||||||
if onlyVisible {
|
|
||||||
query += " WHERE visible=true"
|
|
||||||
}
|
|
||||||
|
|
||||||
var count int
|
|
||||||
err := repo.db.Get(&count, query)
|
|
||||||
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetReleaseByID(id string) (*model.Release, error) {
|
|
||||||
var release = model.Release{}
|
|
||||||
err := repo.db.Get(&release, "SELECT * FROM musicrelease WHERE id=$1", id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
return &release, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetReleaseTracks(releaseID string) ([]*model.Track, error) {
|
|
||||||
var tracks = []*model.Track{}
|
|
||||||
|
|
||||||
err := repo.db.Select(&tracks,
|
|
||||||
"SELECT musictrack.* FROM musictrack "+
|
|
||||||
"JOIN musicreleasetrack ON track=id "+
|
|
||||||
"WHERE release=$1 "+
|
|
||||||
"ORDER BY number ASC",
|
|
||||||
releaseID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tracks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetReleaseCredits(releaseID string) ([]*model.Credit, error) {
|
|
||||||
rows, err := repo.db.Query(
|
|
||||||
"SELECT artist.id,artist.name,artist.website,artist.avatar,role,is_primary "+
|
|
||||||
"FROM musiccredit "+
|
|
||||||
"JOIN artist ON artist=artist.id "+
|
|
||||||
"JOIN musicrelease ON release=musicrelease.id "+
|
|
||||||
"WHERE musicrelease.id=$1 "+
|
|
||||||
"ORDER BY is_primary DESC",
|
|
||||||
releaseID,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var credits []*model.Credit
|
|
||||||
for rows.Next() {
|
|
||||||
credit := &model.Credit{
|
|
||||||
Artist: &model.Artist{},
|
|
||||||
}
|
|
||||||
rows.Scan(
|
|
||||||
&credit.Artist.ID,
|
|
||||||
&credit.Artist.Name,
|
|
||||||
&credit.Artist.Website,
|
|
||||||
&credit.Artist.Avatar,
|
|
||||||
&credit.Role,
|
|
||||||
&credit.Primary)
|
|
||||||
credits = append(credits, credit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return credits, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetReleaseLinks(releaseID string) ([]*model.Link, error) {
|
|
||||||
var links = []*model.Link{}
|
|
||||||
|
|
||||||
err := repo.db.Select(&links, "SELECT name,url FROM musiclink WHERE release=$1", releaseID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return links, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) CreateRelease(
|
|
||||||
id string,
|
|
||||||
title string,
|
|
||||||
releaseType model.ReleaseType,
|
|
||||||
releaseDate time.Time,
|
|
||||||
artworkURL string,
|
|
||||||
) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"INSERT INTO musicrelease "+
|
|
||||||
"(id, title, type, release_date, artwork) "+
|
|
||||||
"VALUES ($1, $2, $3, $4, $5)",
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
releaseType,
|
|
||||||
releaseDate.Format("2006-01-02 15:04:05"),
|
|
||||||
artworkURL,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateRelease(release *model.Release) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE musicrelease SET "+
|
|
||||||
"visible=$2, title=$3, description=$4, type=$5, release_date=$6, artwork=$7, buyname=$8, buylink=$9, copyright=$10, copyrighturl=$11 "+
|
|
||||||
"WHERE id=$1",
|
|
||||||
release.ID,
|
|
||||||
release.Visible,
|
|
||||||
release.Title,
|
|
||||||
release.Description,
|
|
||||||
release.ReleaseType,
|
|
||||||
release.ReleaseDate.Format("2006-01-02 15:04:05"),
|
|
||||||
release.Artwork,
|
|
||||||
release.Buyname,
|
|
||||||
release.Buylink,
|
|
||||||
release.Copyright,
|
|
||||||
release.CopyrightURL,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseID(oldID string, newID string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musicrelease SET id=$2 WHERE id=$1", oldID, newID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseVisibility(id string, visible bool) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musicrelease SET visible=$2 WHERE id=$1", id, visible)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseTitle(id string, title string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musicrelease SET title=$2 WHERE id=$1", id, title)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseDescription(id string, description string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musicrelease SET description=$2 WHERE id=$1", id, description)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseType(id string, releaseType model.ReleaseType) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musicrelease SET type=$2 WHERE id=$1", id, releaseType)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseDate(id string, releaseDate time.Time) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE musicrelease SET release_date=$2 WHERE id=$1",
|
|
||||||
id,
|
|
||||||
releaseDate.Format("2006-01-02 15:04:05"),
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseArtwork(id string, artwork string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musicrelease SET artwork=$2 WHERE id=$1", id, artwork)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseBuyInfo(id string, buyName string, buyLink string) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE musicrelease SET buyname=$2,buylink=$3 WHERE id=$1",
|
|
||||||
id, buyName, buyLink,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseCopyright(id string, copyright string, url string) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE musicrelease SET copyright=$2,copyrighturl=$3 WHERE id=$1",
|
|
||||||
id, copyright, url,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseTracks(releaseID string, newTrackIDs []string) error {
|
|
||||||
tx, err := repo.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM musicreleasetrack WHERE release=$1", releaseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for i, trackID := range newTrackIDs {
|
|
||||||
_, err = tx.Exec(
|
|
||||||
"INSERT INTO musicreleasetrack "+
|
|
||||||
"(release, track, number) "+
|
|
||||||
"VALUES ($1, $2, $3)",
|
|
||||||
releaseID,
|
|
||||||
trackID,
|
|
||||||
i)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseCredits(releaseID string, newCredits []*model.Credit) error {
|
|
||||||
tx, err := repo.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM musiccredit WHERE release=$1", releaseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, credit := range newCredits {
|
|
||||||
_, err = tx.Exec(
|
|
||||||
"INSERT INTO musiccredit "+
|
|
||||||
"(release, artist, role, is_primary) "+
|
|
||||||
"VALUES ($1, $2, $3, $4)",
|
|
||||||
releaseID,
|
|
||||||
credit.Artist.ID,
|
|
||||||
credit.Role,
|
|
||||||
credit.Primary,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateReleaseLinks(releaseID string, newLinks []*model.Link) error {
|
|
||||||
tx, err := repo.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM musiclink WHERE release=$1", releaseID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, link := range newLinks {
|
|
||||||
_, err := tx.Exec(
|
|
||||||
"INSERT INTO musiclink "+
|
|
||||||
"(release, name, url) "+
|
|
||||||
"VALUES ($1, $2, $3)",
|
|
||||||
releaseID,
|
|
||||||
link.Name,
|
|
||||||
link.URL,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = tx.Commit()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) DeleteRelease(id string) (string, error) {
|
|
||||||
var deletedID string
|
|
||||||
err := repo.db.Get(&deletedID, "DELETE FROM musicrelease WHERE id=$1", id)
|
|
||||||
return deletedID, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetAllTracks() ([]*model.Track, error) {
|
|
||||||
return repo.tracks, nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) GetTrackCount() (int, error) {
|
|
||||||
return len(repo.tracks), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetTrackByID(id string) (*model.Track, error) {
|
|
||||||
index := slices.IndexFunc(repo.tracks, func(track *model.Track) bool {
|
|
||||||
return track.ID == id
|
|
||||||
})
|
|
||||||
if index == -1 { return nil, nil }
|
|
||||||
return repo.tracks[index], nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetOrphanTracks() ([]*model.Track, error) {
|
|
||||||
return slices.DeleteFunc(repo.tracks, func(track *model.Track) bool {
|
|
||||||
return slices.ContainsFunc(repo.releases, func(release *model.Release) bool {
|
|
||||||
return slices.ContainsFunc(release.Tracks, func(releaseTrack *model.Track) bool {
|
|
||||||
return releaseTrack.ID == track.ID
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetTracksNotOnRelease(releaseID string) ([]*model.Track, error) {
|
|
||||||
release, err := repo.GetReleaseByID(releaseID)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
|
|
||||||
return slices.DeleteFunc(repo.tracks, func(track *model.Track) bool {
|
|
||||||
return slices.ContainsFunc(release.Tracks, func(releaseTrack *model.Track) bool {
|
|
||||||
return releaseTrack.ID == track.ID
|
|
||||||
})
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) GetTrackReleases(trackID string) ([]*model.Release, error) {
|
|
||||||
return slices.DeleteFunc(repo.releases, func(release *model.Release) bool {
|
|
||||||
return !slices.ContainsFunc(release.Tracks, func(track *model.Track) bool {
|
|
||||||
return track.ID == trackID
|
|
||||||
})
|
|
||||||
}), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) CreateTrack(
|
|
||||||
title string,
|
|
||||||
description string,
|
|
||||||
lyrics string,
|
|
||||||
previewURL string,
|
|
||||||
) (string, error) {
|
|
||||||
id := strconv.Itoa(len(repo.tracks))
|
|
||||||
repo.tracks = append(repo.tracks, &model.Track{
|
|
||||||
ID: id,
|
|
||||||
Title: title,
|
|
||||||
Description: description,
|
|
||||||
Lyrics: lyrics,
|
|
||||||
PreviewURL: previewURL,
|
|
||||||
})
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateTrack(track *model.Track) error {
|
|
||||||
repoTrack, err := repo.GetTrackByID(track.ID)
|
|
||||||
if err != nil { return err }
|
|
||||||
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
|
|
||||||
|
|
||||||
repoTrack.Title = track.Title
|
|
||||||
repoTrack.Description = track.Description
|
|
||||||
repoTrack.Lyrics = track.Lyrics
|
|
||||||
repoTrack.PreviewURL = track.PreviewURL
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateTrackTitle(id string, title string) error {
|
|
||||||
repoTrack, err := repo.GetTrackByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
|
|
||||||
repoTrack.Title = title
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateTrackDescription(id string, description string) error {
|
|
||||||
repoTrack, err := repo.GetTrackByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
|
|
||||||
repoTrack.Description = description
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateTrackLyrics(id string, lyrics string) error {
|
|
||||||
repoTrack, err := repo.GetTrackByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
|
|
||||||
repoTrack.Lyrics = lyrics
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryMemory) UpdateTrackPreviewURL(id string, previewURL string) error {
|
|
||||||
repoTrack, err := repo.GetTrackByID(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if repoTrack == nil { return errors.NewNotExistError("Track does not exist") }
|
|
||||||
repoTrack.PreviewURL = previewURL
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryMemory) DeleteTrack(id string) (string, error) {
|
|
||||||
var deletedID string
|
|
||||||
newTracks := []*model.Track{}
|
|
||||||
for _, track := range repo.tracks {
|
|
||||||
if track.ID == id {
|
|
||||||
deletedID = id
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
newTracks = append(newTracks, track)
|
|
||||||
}
|
|
||||||
repo.tracks = newTracks
|
|
||||||
return deletedID, nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,137 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetAllTracks() ([]*model.Track, error) {
|
|
||||||
var tracks = []*model.Track{}
|
|
||||||
|
|
||||||
err := repo.db.Select(&tracks, "SELECT * FROM musictrack")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tracks, nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) GetTrackCount() (int, error) {
|
|
||||||
var count int
|
|
||||||
err := repo.db.Get(&count, "SELECT count(*) FROM musictrack")
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetTrackByID(id string) (*model.Track, error) {
|
|
||||||
var track = model.Track{}
|
|
||||||
|
|
||||||
stmt, _ := repo.db.Preparex("SELECT * FROM musictrack WHERE id=$1")
|
|
||||||
err := stmt.Get(&track, id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &track, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetOrphanTracks() ([]*model.Track, error) {
|
|
||||||
var tracks = []*model.Track{}
|
|
||||||
|
|
||||||
err := repo.db.Select(&tracks, "SELECT * FROM musictrack WHERE id NOT IN (SELECT track FROM musicreleasetrack)")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tracks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetTracksNotOnRelease(releaseID string) ([]*model.Track, error) {
|
|
||||||
var tracks = []*model.Track{}
|
|
||||||
|
|
||||||
err := repo.db.Select(&tracks,
|
|
||||||
"SELECT * FROM musictrack "+
|
|
||||||
"WHERE id NOT IN "+
|
|
||||||
"(SELECT track FROM musicreleasetrack WHERE release=$1)",
|
|
||||||
releaseID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tracks, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) GetTrackReleases(trackID string) ([]*model.Release, error) {
|
|
||||||
var releases = []*model.Release{}
|
|
||||||
|
|
||||||
err := repo.db.Select(&releases,
|
|
||||||
"SELECT id,title,type,release_date,artwork,buylink "+
|
|
||||||
"FROM musicrelease "+
|
|
||||||
"JOIN musicreleasetrack ON release=id "+
|
|
||||||
"WHERE track=$1 "+
|
|
||||||
"ORDER BY release_date",
|
|
||||||
trackID,
|
|
||||||
)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
|
|
||||||
return releases, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) CreateTrack(
|
|
||||||
title string,
|
|
||||||
description string,
|
|
||||||
lyrics string,
|
|
||||||
previewURL string,
|
|
||||||
) (string, error) {
|
|
||||||
var trackID string
|
|
||||||
err := repo.db.QueryRow(
|
|
||||||
"INSERT INTO musictrack (title, description, lyrics, preview_url) "+
|
|
||||||
"VALUES ($1, $2, $3, $4) "+
|
|
||||||
"RETURNING id",
|
|
||||||
title,
|
|
||||||
description,
|
|
||||||
lyrics,
|
|
||||||
previewURL,
|
|
||||||
).Scan(&trackID)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return trackID, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateTrack(track *model.Track) error {
|
|
||||||
_, err := repo.db.Exec(
|
|
||||||
"UPDATE musictrack "+
|
|
||||||
"SET title=$2, description=$3, lyrics=$4, preview_url=$5 "+
|
|
||||||
"WHERE id=$1",
|
|
||||||
track.ID,
|
|
||||||
track.Title,
|
|
||||||
track.Description,
|
|
||||||
track.Lyrics,
|
|
||||||
track.PreviewURL,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateTrackTitle(id string, title string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musictrack SET title=$2 WHERE id=$1", id, title)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateTrackDescription(id string, description string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musictrack SET description=$2 WHERE id=$1", id, description)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateTrackLyrics(id string, lyrics string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musictrack SET lyrics=$2 WHERE id=$1", id, lyrics)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
func (repo *MusicRepositoryPostgres) UpdateTrackPreviewURL(id string, previewURL string) error {
|
|
||||||
_, err := repo.db.Exec("UPDATE musictrack SET preview_url=$2 WHERE id=$1", id, previewURL)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (repo *MusicRepositoryPostgres) DeleteTrack(id string) (string, error) {
|
|
||||||
var deletedID string
|
|
||||||
err := repo.db.Get(&deletedID, "DELETE FROM musictrack WHERE id=$1 RETURNING id", id)
|
|
||||||
return deletedID, err
|
|
||||||
}
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
package account
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
repository "arimelody-web/repository/account"
|
|
||||||
"arimelody-web/service/validator"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type AccountService struct {
|
|
||||||
repo repository.AccountRepository
|
|
||||||
log *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAccountService(repo repository.AccountRepository, logger *log.Logger) (*AccountService) {
|
|
||||||
return &AccountService{
|
|
||||||
repo: repo,
|
|
||||||
log: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) GetAll() ([]*model.Account, error) {
|
|
||||||
return s.repo.GetAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) GetCount() (int, error) {
|
|
||||||
return s.repo.GetCount()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) GetByID(id string) (*model.Account, error) {
|
|
||||||
account, err := s.repo.GetByID(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) GetByUsername(username string) (*model.Account, error) {
|
|
||||||
account, err := s.repo.GetByUsername(username)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) GetByEmail(email string) (*model.Account, error) {
|
|
||||||
account, err := s.repo.GetByEmail(email)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
if account == nil { return nil, errors.NewNotExistError("Account does not exist") }
|
|
||||||
return account, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) Create(
|
|
||||||
username string,
|
|
||||||
password string,
|
|
||||||
email *string,
|
|
||||||
avatarURL *string,
|
|
||||||
) (string, error) {
|
|
||||||
if len(username) == 0 { return "", errors.NewValidationError("Username cannot be empty") }
|
|
||||||
if !validator.ValidateID(username) { return "", errors.NewValidationError("Username contains invalid characters") }
|
|
||||||
if len(password) == 0 { return "", errors.NewValidationError("Password cannot be empty") }
|
|
||||||
if email != nil && len(*email) == 0 { return "", errors.NewValidationError("Email cannot be empty") }
|
|
||||||
|
|
||||||
var id string
|
|
||||||
var err error
|
|
||||||
if id, err = s.repo.Create(username, password, email, avatarURL); err != nil {
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
s.log.Printf("Created account '%s' (%s)", username, id)
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) ChangeUsername(id string, username string) error {
|
|
||||||
if len(username) == 0 { return errors.NewValidationError("Username cannot be empty") }
|
|
||||||
if !validator.ValidateID(username) { return errors.NewValidationError("Username contains invalid characters") }
|
|
||||||
if err := s.repo.UpdateUsername(id, username); err != nil { return err }
|
|
||||||
s.log.Printf("Changed username for %s to '%s'", id, username)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *AccountService) ChangePassword(id string, password string) error {
|
|
||||||
if len(password) == 0 { return errors.NewValidationError("Password cannot be empty") }
|
|
||||||
if err := s.repo.UpdatePassword(id, password); err != nil { return err }
|
|
||||||
s.log.Printf("Changed password for %s", id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *AccountService) ChangeEmail(id string, email string) error {
|
|
||||||
if len(email) == 0 { return s.repo.RemoveEmail(id) }
|
|
||||||
if err := s.repo.UpdateEmail(id, email); err != nil { return err }
|
|
||||||
s.log.Printf("Changed email for %s to '%s'", id, email)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *AccountService) ChangeAvatarURL(id string, avatarURL string) error {
|
|
||||||
if len(avatarURL) == 0 { return s.repo.RemoveAvatar(id) }
|
|
||||||
if err := s.repo.UpdateAvatarURL(id, avatarURL); err != nil { return err }
|
|
||||||
s.log.Printf("Changed avatar URL for %s to '%s'", id, avatarURL)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) IncrementFails(id string) (int, error) {
|
|
||||||
num, err := s.repo.IncrementFails(id)
|
|
||||||
if err != nil { return 0, err }
|
|
||||||
s.log.Printf("Incremented auth failures for account %s (now %d)", id, num)
|
|
||||||
return num, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) ResetFails(id string) (error) {
|
|
||||||
if err := s.repo.ResetFails(id); err != nil { return err }
|
|
||||||
s.log.Printf("Reset auth failures for account %s", id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) Lock(id string) error {
|
|
||||||
if err := s.repo.SetLocked(id, true); err != nil { return err }
|
|
||||||
s.log.Printf("Locked account %s", id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) Unlock(id string) error {
|
|
||||||
if err := s.repo.SetLocked(id, false); err != nil { return err }
|
|
||||||
s.log.Printf("Unlocked account %s", id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *AccountService) Delete(id string) error {
|
|
||||||
deletedID, err := s.repo.Delete(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if deletedID == "" { return errors.NewNotExistError("Account does not exist") }
|
|
||||||
s.log.Printf("Deleted account %s", id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,407 +0,0 @@
|
||||||
package account_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
repository "arimelody-web/repository/account"
|
|
||||||
service "arimelody-web/service/account"
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
s *service.AccountService
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
devNullFile, err := os.OpenFile(os.DevNull, os.O_RDWR, 0666)
|
|
||||||
if err != nil { panic(err) }
|
|
||||||
defer devNullFile.Close()
|
|
||||||
|
|
||||||
repo := repository.NewAccountRepositoryMemory(make([]*model.Account, 0))
|
|
||||||
s = service.NewAccountService(
|
|
||||||
repo,
|
|
||||||
log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests the account lifecycle. Users:
|
|
||||||
// - [x] can create account
|
|
||||||
// - [x] can't create duplicate account
|
|
||||||
// - [x] can change username
|
|
||||||
// - [x] can change password
|
|
||||||
// - [x] can change email
|
|
||||||
// - [x] can change avatar URL
|
|
||||||
// - [x] can increment auth failures
|
|
||||||
// - [x] can reset auth failures
|
|
||||||
// - [x] can lock account
|
|
||||||
// - [x] can unlock account
|
|
||||||
// - [x] can delete account
|
|
||||||
func Test_Account(t *testing.T) {
|
|
||||||
username := "testificate"
|
|
||||||
password := "the amazing digital data breach"
|
|
||||||
email := "goober@arimelody.space"
|
|
||||||
avatarURL := "/img/account-avatar.webp"
|
|
||||||
|
|
||||||
var id string
|
|
||||||
var err error
|
|
||||||
|
|
||||||
t.Run("accounts should start empty", func(t *testing.T) {
|
|
||||||
t.Run("count is zero", func(t *testing.T) {
|
|
||||||
if num, err := s.GetCount(); err != nil {
|
|
||||||
t.Errorf("Failed to get number of accounts: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, num, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("service returns empty array", func(t *testing.T) {
|
|
||||||
if accounts, err := s.GetAll(); err != nil {
|
|
||||||
t.Errorf("Failed to get accounts: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, len(accounts), 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can create account", func(t *testing.T) {
|
|
||||||
id, err = s.Create(username, password, &email, &avatarURL)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to create account: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("but not with invalid username", func(t *testing.T) {
|
|
||||||
if _, err := s.Create("", password, &email, &avatarURL); err == nil {
|
|
||||||
t.Error("Could create account with invalid username")
|
|
||||||
} else if !errors.IsValidationError(err) {
|
|
||||||
t.Error("Error is not validation error")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("but not with invalid password", func(t *testing.T) {
|
|
||||||
if _, err := s.Create("test-username", "", &email, &avatarURL); err == nil {
|
|
||||||
t.Error("Could create account with invalid password")
|
|
||||||
} else if !errors.IsValidationError(err) {
|
|
||||||
t.Error("Error is not validation error")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("and fetch by ID", func(t *testing.T) {
|
|
||||||
account, err := s.GetByID(id)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to get account after creation: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, account.Username, username)
|
|
||||||
assert.Equal(t, account.Password, password)
|
|
||||||
assert.Equal(t, account.Email.String, email)
|
|
||||||
assert.Equal(t, account.AvatarURL.String, avatarURL)
|
|
||||||
assert.Equal(t, account.FailAttempts, 0)
|
|
||||||
assert.Equal(t, account.Locked, false)
|
|
||||||
})
|
|
||||||
t.Run("and fetch by username", func(t *testing.T) {
|
|
||||||
account, err := s.GetByUsername(username)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to get account after creation: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, account.ID, id)
|
|
||||||
assert.Equal(t, account.Password, password)
|
|
||||||
assert.Equal(t, account.Email.String, email)
|
|
||||||
assert.Equal(t, account.AvatarURL.String, avatarURL)
|
|
||||||
assert.Equal(t, account.FailAttempts, 0)
|
|
||||||
assert.Equal(t, account.Locked, false)
|
|
||||||
})
|
|
||||||
t.Run("and fetch by email", func(t *testing.T) {
|
|
||||||
account, err := s.GetByEmail(email)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Failed to get account after creation: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, account.ID, id)
|
|
||||||
assert.Equal(t, account.Username, username)
|
|
||||||
assert.Equal(t, account.Password, password)
|
|
||||||
assert.Equal(t, account.AvatarURL.String, avatarURL)
|
|
||||||
assert.Equal(t, account.FailAttempts, 0)
|
|
||||||
assert.Equal(t, account.Locked, false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("number of accounts should increment", func(t *testing.T) {
|
|
||||||
t.Run("count is one", func(t *testing.T) {
|
|
||||||
if num, err := s.GetCount(); err != nil {
|
|
||||||
t.Errorf("Failed to get number of accounts: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, num, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("service returns array with one account", func(t *testing.T) {
|
|
||||||
if accounts, err := s.GetAll(); err != nil {
|
|
||||||
t.Errorf("Failed to get accounts: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, len(accounts), 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can't create duplicate account", func(t *testing.T) {
|
|
||||||
_, err := s.Create(username, password, &email, &avatarURL)
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Duplicate account was created")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can change username", func(t *testing.T) {
|
|
||||||
testUsername := "some_other_name"
|
|
||||||
if err := s.ChangeUsername(id, testUsername); err != nil {
|
|
||||||
t.Errorf("Failed to change username: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else if account.Username != testUsername {
|
|
||||||
t.Error("Username did not update")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("but not to an invalid value", func(t *testing.T) {
|
|
||||||
if err := s.ChangeUsername(id, ""); err == nil {
|
|
||||||
t.Error("Could change username to invalid value")
|
|
||||||
} else if !errors.IsValidationError(err) {
|
|
||||||
t.Error("Error is not validation error")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can change password", func(t *testing.T) {
|
|
||||||
testPassword := "other more different password"
|
|
||||||
if err := s.ChangePassword(id, testPassword); err != nil {
|
|
||||||
t.Errorf("Failed to change password: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else if account.Password != testPassword {
|
|
||||||
t.Error("Password did not update")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("but not to an invalid value", func(t *testing.T) {
|
|
||||||
if err := s.ChangePassword(id, ""); err == nil {
|
|
||||||
t.Error("Could change password to invalid value")
|
|
||||||
} else if !errors.IsValidationError(err) {
|
|
||||||
t.Error("Error is not validation error")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can change email", func(t *testing.T) {
|
|
||||||
testEmail := "brandnewemail@for.me"
|
|
||||||
if err := s.ChangeEmail(id, testEmail); err != nil {
|
|
||||||
t.Errorf("Failed to change email: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else if !account.Email.Valid || account.Email.String != testEmail {
|
|
||||||
t.Error("Email did not update")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can remove email", func(t *testing.T) {
|
|
||||||
if err := s.ChangeEmail(id, ""); err != nil {
|
|
||||||
t.Errorf("Failed to change email: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else if account.Email.Valid || len(account.Email.String) > 0 {
|
|
||||||
t.Error("Email did not update")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can change avatar URL", func(t *testing.T) {
|
|
||||||
testAvatarURL := "/img/some-other-avatar.webp"
|
|
||||||
if err := s.ChangeAvatarURL(id, testAvatarURL); err != nil {
|
|
||||||
t.Errorf("Failed to change avatar URL: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else if !account.AvatarURL.Valid || account.AvatarURL.String != testAvatarURL {
|
|
||||||
t.Error("Avatar URL did not update")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can remove avatar URL", func(t *testing.T) {
|
|
||||||
if err := s.ChangeAvatarURL(id, ""); err != nil {
|
|
||||||
t.Errorf("Failed to change avatar URL: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else if account.AvatarURL.Valid || len(account.AvatarURL.String) > 0 {
|
|
||||||
t.Error("Avatar URL did not update")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can increment auth failures", func(t *testing.T) {
|
|
||||||
if num, err := s.IncrementFails(id); err != nil {
|
|
||||||
t.Errorf("Failed to increment account auth failures: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, num, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, account.FailAttempts, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can reset auth failures", func(t *testing.T) {
|
|
||||||
if err := s.ResetFails(id); err != nil {
|
|
||||||
t.Errorf("Failed to reset account auth failures: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, account.FailAttempts, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can lock account", func(t *testing.T) {
|
|
||||||
if err := s.Lock(id); err != nil {
|
|
||||||
t.Errorf("Failed to lock account: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, account.Locked, true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can unlock account", func(t *testing.T) {
|
|
||||||
if err := s.Unlock(id); err != nil {
|
|
||||||
t.Errorf("Failed to unlock account: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
t.Errorf("Failed to get account: %v", err)
|
|
||||||
} else if account == nil {
|
|
||||||
t.Error("Account is nil after update")
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, account.Locked, false)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can delete account", func(t *testing.T) {
|
|
||||||
if err = s.Delete(id); err != nil {
|
|
||||||
t.Errorf("Failed to delete account: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if account, err := s.GetByID(id); err != nil {
|
|
||||||
if !errors.IsNotExistError(err) {
|
|
||||||
t.Errorf("Failed to get account after deletion: %v", err)
|
|
||||||
}
|
|
||||||
} else if account != nil {
|
|
||||||
t.Error("Account still exists after deletion")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can't create an account with invalid", func(t *testing.T) {
|
|
||||||
t.Run("username", func(t *testing.T) {
|
|
||||||
if _, err := s.Create("", password, &email, &avatarURL); err == nil {
|
|
||||||
t.Error("Could create account with empty username")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("password", func(t *testing.T) {
|
|
||||||
if _, err := s.Create(username, "", &email, &avatarURL); err == nil {
|
|
||||||
t.Error("Could create account with empty password")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("email", func(t *testing.T) {
|
|
||||||
testEmail := ""
|
|
||||||
if _, err := s.Create(username, password, &testEmail, &avatarURL); err == nil {
|
|
||||||
t.Error("Could create account with empty (non-nil) email")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can't fetch account that doesn't exist", func(t *testing.T) {
|
|
||||||
t.Run("by ID", func(t *testing.T) {
|
|
||||||
if account, err := s.GetByID("adsginh534g9405gmb40i9bm"); err != nil {
|
|
||||||
if !errors.IsNotExistError(err) { t.Errorf("Failed to get account: %v", err) }
|
|
||||||
} else if account != nil {
|
|
||||||
t.Error("Could fetch non-existent account")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("by username", func(t *testing.T) {
|
|
||||||
if account, err := s.GetByUsername("adsginh534g9405gmb40i9bm"); err != nil {
|
|
||||||
if !errors.IsNotExistError(err) { t.Errorf("Failed to get account: %v", err) }
|
|
||||||
} else if account != nil {
|
|
||||||
t.Error("Could fetch non-existent account")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("email", func(t *testing.T) {
|
|
||||||
if account, err := s.GetByEmail("adsginh534g9405gmb40i9bm"); err != nil {
|
|
||||||
if !errors.IsNotExistError(err) { t.Errorf("Failed to get account: %v", err) }
|
|
||||||
} else if account != nil {
|
|
||||||
t.Error("Could fetch non-existent account")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can't update account that doesn't exist", func(t *testing.T) {
|
|
||||||
garbageAccountID := "adsginh534g9405gmb40i9bm"
|
|
||||||
|
|
||||||
t.Run("username", func(t *testing.T) {
|
|
||||||
if err := s.ChangeUsername(garbageAccountID, "some-username"); err == nil {
|
|
||||||
t.Error("Could update non-existent account's username")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("password", func(t *testing.T) {
|
|
||||||
if err := s.ChangePassword(garbageAccountID, "some-password"); err == nil {
|
|
||||||
t.Error("Could update non-existent account's password")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("email", func(t *testing.T) {
|
|
||||||
if err := s.ChangeEmail(garbageAccountID, "some-email@real.gov"); err == nil {
|
|
||||||
t.Error("Could update non-existent account's email")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("avatar URL", func(t *testing.T) {
|
|
||||||
if err := s.ChangeAvatarURL(garbageAccountID, "/img/null.webp"); err == nil {
|
|
||||||
t.Error("Could update non-existent account's avatar URL")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
package log
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
repository "arimelody-web/repository/log"
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type LogService struct {
|
|
||||||
repo repository.LogRepository
|
|
||||||
log *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLogService(repo repository.LogRepository, logger *log.Logger) *LogService {
|
|
||||||
return &LogService{
|
|
||||||
repo: repo,
|
|
||||||
log: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_LOG_PAGE_LENGTH = 25
|
|
||||||
|
|
||||||
func (s *LogService) Info(logType string, format string, args ...any) {
|
|
||||||
logString := fmt.Sprintf(format, args...)
|
|
||||||
|
|
||||||
s.log.Printf("[%s] [%s] INFO: %s\n", time.Now().Format(time.UnixDate), logType, logString)
|
|
||||||
|
|
||||||
if err := s.repo.Create(model.LEVEL_INFO, logType, logString); err != nil {
|
|
||||||
log.Printf("WARN: Failed to push log to database: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LogService) Warn(logType string, format string, args ...any) {
|
|
||||||
logString := fmt.Sprintf(format, args...)
|
|
||||||
|
|
||||||
log.Printf("[%s] [%s] WARN: %s\n", time.Now().Format(time.UnixDate), logType, logString)
|
|
||||||
|
|
||||||
if err := s.repo.Create(model.LEVEL_INFO, logType, logString); err != nil {
|
|
||||||
log.Printf("WARN: Failed to push log to database: %v\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LogService) Fetch(id string) (*model.Log, error) {
|
|
||||||
log, err := s.repo.Get(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
if log == nil { return nil, errors.NewNotExistError("Log does not exist") }
|
|
||||||
return log, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *LogService) Search(
|
|
||||||
levelFilters []model.LogLevel,
|
|
||||||
typeFilters []string,
|
|
||||||
content string,
|
|
||||||
limit int,
|
|
||||||
offset int,
|
|
||||||
) ([]*model.Log, error) {
|
|
||||||
return s.repo.Search(levelFilters, typeFilters, content, limit, offset)
|
|
||||||
}
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
"arimelody-web/service/validator"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *MusicService) GetAllArtists() ([]*model.Artist, error) {
|
|
||||||
return s.repo.GetAllArtists()
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetArtistCount() (int, error) {
|
|
||||||
return s.repo.GetArtistCount()
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetArtistByID(id string) (*model.Artist, error) {
|
|
||||||
artist, err := s.repo.GetArtistByID(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
if artist == nil { return nil, errors.NewNotExistError("Artist does not exist") }
|
|
||||||
return artist, nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetArtistsNotOnRelease(releaseID string) ([]*model.Artist, error) {
|
|
||||||
return s.repo.GetArtistsNotOnRelease(releaseID)
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetArtistCredits(artistID string, showHidden bool) ([]*model.Credit, error) {
|
|
||||||
return s.repo.GetArtistCredits(artistID, showHidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) CreateArtist(
|
|
||||||
id string,
|
|
||||||
name string,
|
|
||||||
website string,
|
|
||||||
avatar string,
|
|
||||||
) error {
|
|
||||||
if len(id) == 0 { return errors.NewValidationError("Artist ID cannot be empty") }
|
|
||||||
if !validator.ValidateID(id) { return errors.NewValidationError("Artist ID contains invalid characters") }
|
|
||||||
if len(name) == 0 { return errors.NewValidationError("Artist name cannot be empty") }
|
|
||||||
|
|
||||||
if err := s.repo.CreateArtist(id, name, website, avatar); err != nil { return err }
|
|
||||||
s.log.Printf("Created new artist '%s' (%s)", name, id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) UpdateArtist(artist *model.Artist) error {
|
|
||||||
if len(artist.ID) == 0 { return errors.NewValidationError("Artist ID cannot be empty") }
|
|
||||||
if !validator.ValidateID(artist.ID) { return errors.NewValidationError("Artist ID contains invalid characters") }
|
|
||||||
if len(artist.Name) == 0 { return errors.NewValidationError("Artist name cannot be empty") }
|
|
||||||
if err := s.repo.UpdateArtist(artist); err != nil { return err }
|
|
||||||
s.log.Printf("Updated artist %s", artist.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) UpdateArtistID(oldID string, newID string) error {
|
|
||||||
if len(newID) == 0 { return errors.NewValidationError("Artist ID cannot be empty") }
|
|
||||||
if !validator.ValidateID(newID) { return errors.NewValidationError("Artist ID contains invalid characters") }
|
|
||||||
if err := s.repo.UpdateArtistID(oldID, newID); err != nil { return err }
|
|
||||||
s.log.Printf("Updated artist ID %s to %s", oldID, newID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateArtistName(id string, name string) error {
|
|
||||||
if len(name) == 0 { return errors.NewValidationError("Artist name cannot be empty") }
|
|
||||||
if err := s.repo.UpdateArtistName(id, name); err != nil { return err }
|
|
||||||
s.log.Printf("Updated artist %s name to %s", id, name)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateArtistWebsite(id string, website string) error {
|
|
||||||
if err := s.repo.UpdateArtistWebsite(id, website); err != nil { return err }
|
|
||||||
s.log.Printf("Updated artist %s website to %s", id, website)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateArtistAvatar(id string, avatar string) error {
|
|
||||||
if err := s.repo.UpdateArtistAvatar(id, avatar); err != nil { return err }
|
|
||||||
s.log.Printf("Updated artist %s avatar to %s", id, avatar)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) DeleteArtist(id string) error {
|
|
||||||
deletedID, err := s.repo.DeleteArtist(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if deletedID == "" { return errors.NewNotExistError("Artist does not exist") }
|
|
||||||
s.log.Printf("Deleted artist %s", id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
package music_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
repository "arimelody-web/repository/music"
|
|
||||||
service "arimelody-web/service/music"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Artist(t *testing.T) {
|
|
||||||
devNullFile, err := os.OpenFile(os.DevNull, os.O_RDWR, 0666)
|
|
||||||
if err != nil { panic(err) }
|
|
||||||
defer devNullFile.Close()
|
|
||||||
|
|
||||||
repo := repository.NewMusicRepositoryMemory(
|
|
||||||
make([]*model.Artist, 0),
|
|
||||||
make([]*model.Release, 0),
|
|
||||||
make([]*model.Track, 0),
|
|
||||||
)
|
|
||||||
s = service.NewMusicService(
|
|
||||||
repo,
|
|
||||||
log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS),
|
|
||||||
)
|
|
||||||
|
|
||||||
id := "cool-artist"
|
|
||||||
name := "Cool Artist"
|
|
||||||
website := "artist.arimelody.space"
|
|
||||||
avatarURL := "/img/cool-artist.webp"
|
|
||||||
|
|
||||||
t.Run("artists should start empty", func(t *testing.T) {
|
|
||||||
t.Run("count is zero", func(t *testing.T) {
|
|
||||||
if num, err := s.GetArtistCount(); err != nil {
|
|
||||||
t.Errorf("Failed to get number of artists: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, num, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("service returns empty array", func(t *testing.T) {
|
|
||||||
if artists, err := s.GetAllArtists(); err != nil {
|
|
||||||
t.Errorf("Failed to get artists: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, len(artists), 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can create artist", func(t *testing.T) {
|
|
||||||
if err := s.CreateArtist(id, name, website, avatarURL); err != nil {
|
|
||||||
t.Errorf("Failed to create artist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("but not with an invalid ID", func(t *testing.T) {
|
|
||||||
if err := s.CreateArtist("", name, website, avatarURL); err == nil {
|
|
||||||
t.Error("Created artist with invalid ID")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("but not with an invalid name", func(t *testing.T) {
|
|
||||||
if err := s.CreateArtist("test-artist", "", website, avatarURL); err == nil {
|
|
||||||
t.Error("Created artist with invalid name")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("and retrieve it", func(t *testing.T) {
|
|
||||||
repoArtist, err := s.GetArtistByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get artist: %v", err) }
|
|
||||||
|
|
||||||
assert.Equal(t, repoArtist.ID, id)
|
|
||||||
assert.Equal(t, repoArtist.Name, name)
|
|
||||||
assert.Equal(t, repoArtist.Website, website)
|
|
||||||
assert.Equal(t, repoArtist.Avatar, avatarURL)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("number of artists should increment", func(t *testing.T) {
|
|
||||||
t.Run("count is one", func(t *testing.T) {
|
|
||||||
if num, err := s.GetArtistCount(); err != nil {
|
|
||||||
t.Errorf("Failed to get number of artists: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, num, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("service returns array with one artist", func(t *testing.T) {
|
|
||||||
if artists, err := s.GetAllArtists(); err != nil {
|
|
||||||
t.Errorf("Failed to get artists: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, len(artists), 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
releaseID := "test-release"
|
|
||||||
t.Run("can get artists not on a release", func(t *testing.T) {
|
|
||||||
if err := s.CreateRelease(releaseID, "test", string(model.Single), time.Now(), ""); err != nil {
|
|
||||||
t.Errorf("Failed to create release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
artists, err := s.GetArtistsNotOnRelease(releaseID)
|
|
||||||
if err != nil { t.Errorf("Failed to get artists: %v", err) }
|
|
||||||
|
|
||||||
assert.Assert(t, slices.ContainsFunc(artists, func(artist *model.Artist) bool {
|
|
||||||
return artist.ID == id
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
|
|
||||||
credit := &model.Credit{
|
|
||||||
Artist: &model.Artist{ ID: id, Name: name, Website: website, Avatar: avatarURL },
|
|
||||||
Role: "did a lot of stuff",
|
|
||||||
Primary: true,
|
|
||||||
}
|
|
||||||
t.Run("can get artist credits", func(t *testing.T) {
|
|
||||||
if err := s.UpdateReleaseCredits(releaseID, []*model.Credit{ credit }); err != nil {
|
|
||||||
t.Errorf("Failed to update release credits: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
credits, err := s.GetArtistCredits(id, true)
|
|
||||||
if err != nil { t.Errorf("Failed to get credits: %v", err) }
|
|
||||||
|
|
||||||
index := slices.IndexFunc(credits, func(credit *model.Credit) bool {
|
|
||||||
return credit.Artist.ID == id
|
|
||||||
})
|
|
||||||
assert.Assert(t, index != -1)
|
|
||||||
assert.Equal(t, credits[index].Artist.ID, id)
|
|
||||||
assert.Equal(t, credits[index].Artist.Name, name)
|
|
||||||
assert.Equal(t, credits[index].Artist.Website, website)
|
|
||||||
assert.Equal(t, credits[index].Artist.Avatar, avatarURL)
|
|
||||||
assert.Equal(t, credits[index].Role, credit.Role)
|
|
||||||
assert.Equal(t, credits[index].Primary, credit.Primary)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update artist", func(t *testing.T) {
|
|
||||||
testName := "this name is only temporary"
|
|
||||||
testWebsite := "https://test.example.org"
|
|
||||||
testAvatar := "/img/test-avatar.webp"
|
|
||||||
if err := s.UpdateArtist(&model.Artist{
|
|
||||||
ID: id,
|
|
||||||
Name: testName,
|
|
||||||
Website: testWebsite,
|
|
||||||
Avatar: testAvatar,
|
|
||||||
}); err != nil {
|
|
||||||
t.Errorf("Failed to update artist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
artist, err := s.GetArtistByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get artist: %v", err) }
|
|
||||||
assert.Equal(t, artist.Name, testName)
|
|
||||||
assert.Equal(t, artist.Website, testWebsite)
|
|
||||||
assert.Equal(t, artist.GetAvatar(), testAvatar)
|
|
||||||
|
|
||||||
t.Run("but not with invalid name", func(t *testing.T) {
|
|
||||||
if err := s.UpdateArtist(&model.Artist{
|
|
||||||
ID: id,
|
|
||||||
Name: "",
|
|
||||||
}); err == nil {
|
|
||||||
t.Errorf("Updated artist name to invalid value")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update artist name", func(t *testing.T) {
|
|
||||||
if err := s.UpdateArtistName(id, name); err != nil {
|
|
||||||
t.Errorf("Failed to update artist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
artist, err := s.GetArtistByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get artist: %v", err) }
|
|
||||||
assert.Equal(t, artist.Name, name)
|
|
||||||
|
|
||||||
t.Run("but not with invalid value", func(t *testing.T) {
|
|
||||||
if err := s.UpdateArtistName(id, ""); err == nil {
|
|
||||||
t.Errorf("Updated artist name to invalid value")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update artist website", func(t *testing.T) {
|
|
||||||
if err := s.UpdateArtistWebsite(id, website); err != nil {
|
|
||||||
t.Errorf("Failed to update artist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
artist, err := s.GetArtistByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get artist: %v", err) }
|
|
||||||
assert.Equal(t, artist.Website, website)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update artist avatar", func(t *testing.T) {
|
|
||||||
if err := s.UpdateArtistAvatar(id, ""); err != nil {
|
|
||||||
t.Errorf("Failed to update artist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
artist, err := s.GetArtistByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get artist: %v", err) }
|
|
||||||
assert.Equal(t, artist.GetAvatar(), model.DEFAULT_AVATAR_URL)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can delete artist", func(t *testing.T) {
|
|
||||||
if err := s.DeleteArtist(id); err != nil {
|
|
||||||
t.Errorf("Failed to delete artist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("no longer exists", func(t *testing.T) {
|
|
||||||
if artist, err := s.GetArtistByID(id); err == nil {
|
|
||||||
t.Error("No error getting artist")
|
|
||||||
} else if artist != nil {
|
|
||||||
t.Error("Artist with this ID still exists")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("but not one that doesn't exist", func(t *testing.T) {
|
|
||||||
if err := s.DeleteArtist("some-garbage-id"); err == nil {
|
|
||||||
if !errors.IsNotExistError(err) {
|
|
||||||
t.Errorf("Failed to delete artist: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
repository "arimelody-web/repository/music"
|
|
||||||
"log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type MusicService struct {
|
|
||||||
repo repository.MusicRepository
|
|
||||||
log *log.Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMusicService(repo repository.MusicRepository, logger *log.Logger) *MusicService {
|
|
||||||
return &MusicService{
|
|
||||||
repo: repo,
|
|
||||||
log: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
package music_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
service "arimelody-web/service/music"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
s *service.MusicService
|
|
||||||
)
|
|
||||||
|
|
@ -1,180 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
"arimelody-web/service/validator"
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Hydrates a release with its additional data (credits, tracks, links)
|
|
||||||
func (s *MusicService) fillRelease(release *model.Release) error {
|
|
||||||
credits, err := s.GetReleaseCredits(release.ID)
|
|
||||||
if err != nil { return fmt.Errorf("credits: %s", err) }
|
|
||||||
release.Credits = append(release.Credits, credits...)
|
|
||||||
|
|
||||||
tracks, err := s.GetReleaseTracks(release.ID)
|
|
||||||
if err != nil { return fmt.Errorf("tracks: %s", err) }
|
|
||||||
release.Tracks = append(release.Tracks, tracks...)
|
|
||||||
|
|
||||||
links, err := s.GetReleaseLinks(release.ID)
|
|
||||||
if err != nil { return fmt.Errorf("links: %s", err) }
|
|
||||||
release.Links = append(release.Links, links...)
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) GetAllReleases(onlyVisible bool, limit int) ([]*model.Release, error) {
|
|
||||||
return s.repo.GetAllReleases(onlyVisible, limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) GetAllFullReleases(onlyVisible bool, limit int) ([]*model.Release, error) {
|
|
||||||
releases, err := s.repo.GetAllReleases(onlyVisible, limit)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
for _, release := range releases {
|
|
||||||
if err := s.fillRelease(release); err != nil { return nil, err }
|
|
||||||
}
|
|
||||||
return releases, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) GetReleaseCount(onlyVisible bool) (int, error) {
|
|
||||||
return s.repo.GetReleaseCount(onlyVisible)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) GetReleaseByID(id string) (*model.Release, error) {
|
|
||||||
release, err := s.repo.GetReleaseByID(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
if release == nil { return nil, errors.NewNotExistError("Release does not exist") }
|
|
||||||
return release, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) GetFullReleaseByID(id string) (*model.Release, error) {
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
if err := s.fillRelease(release); err != nil { return nil, err }
|
|
||||||
return release, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) GetReleaseTracks(id string) ([]*model.Track, error) {
|
|
||||||
return s.repo.GetReleaseTracks(id)
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetReleaseCredits(id string) ([]*model.Credit, error) {
|
|
||||||
return s.repo.GetReleaseCredits(id)
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetReleaseLinks(id string) ([]*model.Link, error) {
|
|
||||||
return s.repo.GetReleaseLinks(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) CreateRelease(
|
|
||||||
id string,
|
|
||||||
title string,
|
|
||||||
releaseType string,
|
|
||||||
releaseDate time.Time,
|
|
||||||
artworkURL string,
|
|
||||||
) error {
|
|
||||||
if len(id) == 0 { return errors.NewValidationError("Release ID cannot be empty") }
|
|
||||||
if !validator.ValidateID(id) { return errors.NewValidationError("Release ID contains invalid characters") }
|
|
||||||
if len(title) == 0 { return errors.NewValidationError("Release title cannot be empty") }
|
|
||||||
validReleaseType, ok := model.ValidReleaseType(releaseType)
|
|
||||||
if !ok { return errors.NewValidationError("Invalid release type") }
|
|
||||||
|
|
||||||
if err := s.repo.CreateRelease(id, title, validReleaseType, releaseDate, artworkURL); err != nil { return err }
|
|
||||||
s.log.Printf("Created new release '%s' (%s)", title, id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) UpdateRelease(release *model.Release) error {
|
|
||||||
if len(release.ID) == 0 { return errors.NewValidationError("Release ID cannot be empty") }
|
|
||||||
if !validator.ValidateID(release.ID) { return errors.NewValidationError("Release ID contains invalid characters") }
|
|
||||||
if len(release.Title) == 0 { return errors.NewValidationError("Release title cannot be empty") }
|
|
||||||
|
|
||||||
if err := s.repo.UpdateRelease(release); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' (%s)", release.Title, release.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseID(oldID string, newID string) error {
|
|
||||||
if len(newID) == 0 { return errors.NewValidationError("Release ID cannot be empty") }
|
|
||||||
if !validator.ValidateID(newID) { return errors.NewValidationError("Release ID contains invalid characters") }
|
|
||||||
if err := s.repo.UpdateReleaseID(oldID, newID); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release ID %s to %s", oldID, newID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseVisibility(id string, visible bool) error {
|
|
||||||
if err := s.repo.UpdateReleaseVisibility(id, visible); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' visibility to %t", id, visible)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseTitle(id string, title string) error {
|
|
||||||
if len(title) == 0 { return errors.NewValidationError("Release title cannot be empty") }
|
|
||||||
if err := s.repo.UpdateReleaseTitle(id, title); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' title to %s", id, title)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseDescription(id string, description string) error {
|
|
||||||
if err := s.repo.UpdateReleaseDescription(id, description); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' description to %s", id, description)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseType(id string, releaseType string) error {
|
|
||||||
validReleaseType, ok := model.ValidReleaseType(releaseType)
|
|
||||||
if !ok { return errors.NewValidationError("Invalid release type") }
|
|
||||||
if err := s.repo.UpdateReleaseType(id, validReleaseType); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' type to %s", id, releaseType)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseDate(id string, releaseDate time.Time) error {
|
|
||||||
if err := s.repo.UpdateReleaseDate(id, releaseDate); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' date to %s", id, releaseDate.Format(time.RFC3339))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseArtwork(id string, artwork string) error {
|
|
||||||
if err := s.repo.UpdateReleaseArtwork(id, artwork); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' artwork to %s", id, artwork)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseBuyInfo(id string, buyName string, buyLink string) error {
|
|
||||||
if err := s.repo.UpdateReleaseBuyInfo(id, buyName, buyLink); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' buy info (name='%s', link='%s')", id, buyName, buyLink)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateReleaseCopyright(id string, copyright string, url string) error {
|
|
||||||
if err := s.repo.UpdateReleaseCopyright(id, copyright, url); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' copyright (copyright='%s', url='%s')", id, copyright, url)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) UpdateReleaseTracks(id string, newTrackIDs []string) error {
|
|
||||||
for _, id := range newTrackIDs {
|
|
||||||
if len(id) == 0 { return errors.NewValidationError("Track IDs cannot be empty") }
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.repo.UpdateReleaseTracks(id, newTrackIDs); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' tracks (%d tracks)", id, len(newTrackIDs))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) UpdateReleaseCredits(id string, newCredits []*model.Credit) error {
|
|
||||||
if err := s.repo.UpdateReleaseCredits(id, newCredits); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' credits (%d credits)", id, len(newCredits))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) UpdateReleaseLinks(id string, newLinks []*model.Link) error {
|
|
||||||
for _, link := range newLinks {
|
|
||||||
if len(link.Name) == 0 { return errors.NewValidationError("Link names cannot be empty") }
|
|
||||||
if len(link.URL) == 0 { return errors.NewValidationError("Link URLs cannot be empty") }
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.repo.UpdateReleaseLinks(id, newLinks); err != nil { return err }
|
|
||||||
s.log.Printf("Updated release '%s' links (%d links)", id, len(newLinks))
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) DeleteRelease(id string) error {
|
|
||||||
deletedID, err := s.repo.DeleteRelease(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if deletedID == "" { return errors.NewNotExistError("Release does not exist") }
|
|
||||||
s.log.Printf("Deleted release '%s'", id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,374 +0,0 @@
|
||||||
package music_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/errors"
|
|
||||||
"arimelody-web/model"
|
|
||||||
repository "arimelody-web/repository/music"
|
|
||||||
service "arimelody-web/service/music"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_Release(t *testing.T) {
|
|
||||||
devNullFile, err := os.OpenFile(os.DevNull, os.O_RDWR, 0666)
|
|
||||||
if err != nil { panic(err) }
|
|
||||||
defer devNullFile.Close()
|
|
||||||
|
|
||||||
repo := repository.NewMusicRepositoryMemory(
|
|
||||||
make([]*model.Artist, 0),
|
|
||||||
make([]*model.Release, 0),
|
|
||||||
make([]*model.Track, 0),
|
|
||||||
)
|
|
||||||
s = service.NewMusicService(
|
|
||||||
repo,
|
|
||||||
log.New(devNullFile, "", model.DEFAULT_LOG_FLAGS),
|
|
||||||
)
|
|
||||||
|
|
||||||
id := "cool-release"
|
|
||||||
title := "Cool Release"
|
|
||||||
releaseType := model.Album
|
|
||||||
releaseDate := time.Now()
|
|
||||||
artworkURL := "/img/some-cool-artwork.webp"
|
|
||||||
|
|
||||||
t.Run("releases should start empty", func(t *testing.T) {
|
|
||||||
t.Run("count is zero", func(t *testing.T) {
|
|
||||||
if num, err := s.GetReleaseCount(false); err != nil {
|
|
||||||
t.Errorf("Failed to get number of releases: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, num, 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("service returns empty array", func(t *testing.T) {
|
|
||||||
if releases, err := s.GetAllReleases(false, 0); err != nil {
|
|
||||||
t.Errorf("Failed to get releases: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, len(releases), 0)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can create release", func(t *testing.T) {
|
|
||||||
if err := s.CreateRelease(id, title, string(releaseType), releaseDate, artworkURL); err != nil {
|
|
||||||
t.Errorf("Failed to create release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("but not with an invalid ID", func(t *testing.T) {
|
|
||||||
if err := s.CreateRelease("", title, string(releaseType), releaseDate, artworkURL); err == nil {
|
|
||||||
t.Error("Created release with invalid ID")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("but not with an invalid title", func(t *testing.T) {
|
|
||||||
if err := s.CreateRelease("test-release", "", string(releaseType), releaseDate, artworkURL); err == nil {
|
|
||||||
t.Error("Created release with invalid title")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
t.Run("but not with an invalid type", func(t *testing.T) {
|
|
||||||
if err := s.CreateRelease("test-release", "", "garbage-type", releaseDate, artworkURL); err == nil {
|
|
||||||
t.Error("Created release with invalid type")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("and retrieve it", func(t *testing.T) {
|
|
||||||
repoRelease, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if repoRelease == nil { t.Error("Release does not exist") }
|
|
||||||
|
|
||||||
assert.Equal(t, repoRelease.ID, id)
|
|
||||||
assert.Equal(t, repoRelease.Title, title)
|
|
||||||
assert.Equal(t, repoRelease.ReleaseType, releaseType)
|
|
||||||
assert.Equal(t, repoRelease.ReleaseDate, releaseDate)
|
|
||||||
assert.Equal(t, repoRelease.GetArtwork(), artworkURL)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("should not be visible by default", func(t *testing.T) {
|
|
||||||
releases, err := s.GetAllReleases(true, 0)
|
|
||||||
if err != nil { t.Errorf("Failed to get releases: %v", err) }
|
|
||||||
|
|
||||||
assert.Equal(t, slices.ContainsFunc(releases, func(release *model.Release) bool {
|
|
||||||
return release.ID == id
|
|
||||||
}), false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("number of releases should increment", func(t *testing.T) {
|
|
||||||
t.Run("count is one", func(t *testing.T) {
|
|
||||||
if num, err := s.GetReleaseCount(false); err != nil {
|
|
||||||
t.Errorf("Failed to get number of releases: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, num, 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("service returns array with one release", func(t *testing.T) {
|
|
||||||
if releases, err := s.GetAllReleases(false, 0); err != nil {
|
|
||||||
t.Errorf("Failed to get releases: %v", err)
|
|
||||||
} else {
|
|
||||||
assert.Equal(t, len(releases), 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update release", func(t *testing.T) {
|
|
||||||
testTitle := "this title is only temporary"
|
|
||||||
testReleaseType := model.Compilation
|
|
||||||
testReleaseDate := time.Now().AddDate(0, 0, 10)
|
|
||||||
testArtworkURL := "/img/test-artwork.webp"
|
|
||||||
if err := s.UpdateRelease(&model.Release{
|
|
||||||
ID: id,
|
|
||||||
Title: testTitle,
|
|
||||||
ReleaseType: testReleaseType,
|
|
||||||
ReleaseDate: testReleaseDate,
|
|
||||||
Artwork: testArtworkURL,
|
|
||||||
}); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.Title, testTitle)
|
|
||||||
assert.Equal(t, release.ReleaseType, testReleaseType)
|
|
||||||
assert.Equal(t, release.ReleaseDate, testReleaseDate)
|
|
||||||
assert.Equal(t, release.GetArtwork(), testArtworkURL)
|
|
||||||
|
|
||||||
t.Run("but not with invalid title", func(t *testing.T) {
|
|
||||||
if err := s.UpdateRelease(&model.Release{
|
|
||||||
ID: id,
|
|
||||||
Title: "",
|
|
||||||
}); err == nil {
|
|
||||||
t.Errorf("Updated release title to invalid value")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update visibility", func(t *testing.T) {
|
|
||||||
if err := s.UpdateReleaseVisibility(id, true); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.Visible, true)
|
|
||||||
|
|
||||||
t.Run("should be visible after updating", func(t *testing.T) {
|
|
||||||
releases, err := s.GetAllReleases(true, 0)
|
|
||||||
if err != nil { t.Errorf("Failed to get releases: %v", err) }
|
|
||||||
|
|
||||||
assert.Equal(t, slices.ContainsFunc(releases, func(release *model.Release) bool {
|
|
||||||
return release.ID == id
|
|
||||||
}), true)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update title", func(t *testing.T) {
|
|
||||||
if err := s.UpdateReleaseTitle(id, title); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.Title, title)
|
|
||||||
|
|
||||||
t.Run("but not with invalid value", func(t *testing.T) {
|
|
||||||
if err := s.UpdateReleaseTitle(id, ""); err == nil {
|
|
||||||
t.Errorf("Updated release title to invalid value")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update description", func(t *testing.T) {
|
|
||||||
testDescription := "an incredible and thought-provoking description"
|
|
||||||
if err := s.UpdateReleaseDescription(id, testDescription); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.Description, testDescription)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update type", func(t *testing.T) {
|
|
||||||
testType := model.EP
|
|
||||||
if err := s.UpdateReleaseType(id, string(testType)); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.ReleaseType, testType)
|
|
||||||
releaseType = testType
|
|
||||||
|
|
||||||
t.Run("but not with invalid value", func(t *testing.T) {
|
|
||||||
if err := s.UpdateReleaseType(id, "garbage-type"); err == nil {
|
|
||||||
t.Errorf("Updated release type to invalid value")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update date", func(t *testing.T) {
|
|
||||||
testDate := time.Now().Add(time.Hour * 24)
|
|
||||||
if err := s.UpdateReleaseDate(id, testDate); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.ReleaseDate, testDate)
|
|
||||||
releaseDate = testDate
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update artwork", func(t *testing.T) {
|
|
||||||
if err := s.UpdateReleaseArtwork(id, ""); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.GetArtwork(), model.DEFAULT_RELEASE_ARTWORK_URL)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update buy info", func(t *testing.T) {
|
|
||||||
testBuyName := "get it now!!!"
|
|
||||||
testBuyLink := "https://arimelody.space/music"
|
|
||||||
if err := s.UpdateReleaseBuyInfo(id, testBuyName, testBuyLink); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.Buyname, testBuyName)
|
|
||||||
assert.Equal(t, release.Buylink, testBuyLink)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update copyright info", func(t *testing.T) {
|
|
||||||
testCopyright := "CC BY-SA 4.0"
|
|
||||||
testCopyrightURL := "https://creativecommons.org/licenses/by-sa/4.0/"
|
|
||||||
if err := s.UpdateReleaseCopyright(id, testCopyright, testCopyrightURL); err != nil {
|
|
||||||
t.Errorf("Failed to update release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
release, err := s.GetReleaseByID(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get release: %v", err) }
|
|
||||||
if release == nil { t.Error("Release does not exist after update") }
|
|
||||||
assert.Equal(t, release.Copyright, testCopyright)
|
|
||||||
assert.Equal(t, release.CopyrightURL, testCopyrightURL)
|
|
||||||
})
|
|
||||||
|
|
||||||
track := &model.Track{
|
|
||||||
Title: "test track",
|
|
||||||
Description: "average description",
|
|
||||||
Lyrics: "some lyrics",
|
|
||||||
}
|
|
||||||
t.Run("can update tracks", func(t *testing.T) {
|
|
||||||
if trackID, err := s.CreateTrack(track.Title, track.Description, track.Lyrics, ""); err != nil {
|
|
||||||
t.Errorf("Failed to create track: %v", err)
|
|
||||||
} else { track.ID = trackID }
|
|
||||||
|
|
||||||
if err := s.UpdateReleaseTracks(id, []string{ track.ID }); err != nil {
|
|
||||||
t.Errorf("Failed to update release tracks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks, err := s.GetReleaseTracks(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get tracks: %v", err) }
|
|
||||||
|
|
||||||
index := slices.IndexFunc(tracks, func(repoTrack *model.Track) bool {
|
|
||||||
return repoTrack.ID == track.ID
|
|
||||||
})
|
|
||||||
assert.Assert(t, index != -1)
|
|
||||||
assert.Equal(t, tracks[index].Title, track.Title)
|
|
||||||
assert.Equal(t, tracks[index].Description, track.Description)
|
|
||||||
assert.Equal(t, tracks[index].Lyrics, track.Lyrics)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update credits", func(t *testing.T) {
|
|
||||||
artist := &model.Artist{ ID: id, Name: title, Website: "", Avatar: "" }
|
|
||||||
if err := s.CreateArtist(artist.ID, artist.Name, artist.Website, artist.Avatar); err != nil {
|
|
||||||
t.Errorf("Failed to create artist: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
credit := &model.Credit{
|
|
||||||
Artist: artist,
|
|
||||||
Role: "did a lot of stuff",
|
|
||||||
Primary: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.UpdateReleaseCredits(id, []*model.Credit{ credit }); err != nil {
|
|
||||||
t.Errorf("Failed to update release credits: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
credits, err := s.GetReleaseCredits(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get credits: %v", err) }
|
|
||||||
assert.Equal(t, len(credits), 1)
|
|
||||||
|
|
||||||
index := slices.IndexFunc(credits, func(repoCredit *model.Credit) bool {
|
|
||||||
return repoCredit.Artist.ID == credit.Artist.ID
|
|
||||||
})
|
|
||||||
assert.Assert(t, index != -1)
|
|
||||||
assert.Equal(t, credits[index].Release.ID, id)
|
|
||||||
assert.Equal(t, credits[index].Release.Title, title)
|
|
||||||
assert.Equal(t, credits[index].Release.ReleaseType, releaseType)
|
|
||||||
assert.Equal(t, credits[index].Release.ReleaseDate, releaseDate)
|
|
||||||
assert.Equal(t, credits[index].Release.GetArtwork(), model.DEFAULT_RELEASE_ARTWORK_URL)
|
|
||||||
assert.Equal(t, credits[index].Artist.ID, credit.Artist.ID)
|
|
||||||
assert.Equal(t, credits[index].Artist.Name, credit.Artist.Name)
|
|
||||||
assert.Equal(t, credits[index].Artist.Website, credit.Artist.Website)
|
|
||||||
assert.Equal(t, credits[index].Artist.GetAvatar(), credit.Artist.GetAvatar())
|
|
||||||
assert.Equal(t, credits[index].Role, credit.Role)
|
|
||||||
assert.Equal(t, credits[index].Primary, credit.Primary)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can update links", func(t *testing.T) {
|
|
||||||
link := &model.Link{
|
|
||||||
Name: "awesome link you should totally go here",
|
|
||||||
URL: "https://arimelody.space",
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.UpdateReleaseLinks(id, []*model.Link{ link }); err != nil {
|
|
||||||
t.Errorf("Failed to update release tracks: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
links, err := s.GetReleaseLinks(id)
|
|
||||||
if err != nil { t.Errorf("Failed to get tracks: %v", err) }
|
|
||||||
|
|
||||||
index := slices.IndexFunc(links, func(repoLink *model.Link) bool {
|
|
||||||
return repoLink.Name == link.Name
|
|
||||||
})
|
|
||||||
assert.Assert(t, index != -1)
|
|
||||||
assert.Equal(t, links[index].URL, link.URL)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("can delete release", func(t *testing.T) {
|
|
||||||
if err := s.DeleteRelease(id); err != nil {
|
|
||||||
t.Errorf("Failed to delete release: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("no longer exists", func(t *testing.T) {
|
|
||||||
if release, err := s.GetReleaseByID(id); err == nil {
|
|
||||||
t.Error("No error getting release")
|
|
||||||
} else if release != nil {
|
|
||||||
t.Error("Release with this ID still exists")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("but not one that doesn't exist", func(t *testing.T) {
|
|
||||||
if err := s.DeleteRelease("some-garbage-id"); err == nil {
|
|
||||||
if !errors.IsNotExistError(err) {
|
|
||||||
t.Errorf("Failed to delete release: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
package music
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/model"
|
|
||||||
"arimelody-web/errors"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
func (s *MusicService) GetAllTracks() ([]*model.Track, error) {
|
|
||||||
return s.repo.GetAllTracks()
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetTrackCount() (int, error) {
|
|
||||||
return s.repo.GetTrackCount()
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetTrackByID(id string) (*model.Track, error) {
|
|
||||||
track, err := s.repo.GetTrackByID(id)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
if track == nil { return nil, errors.NewNotExistError("Track does not exist") }
|
|
||||||
return track, nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetOrphanTracks() ([]*model.Track, error) {
|
|
||||||
return s.repo.GetOrphanTracks()
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetTracksNotOnRelease(releaseID string) ([]*model.Track, error) {
|
|
||||||
return s.repo.GetTracksNotOnRelease(releaseID)
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetTrackReleases(trackID string) ([]*model.Release, error) {
|
|
||||||
return s.repo.GetTrackReleases(trackID)
|
|
||||||
}
|
|
||||||
func (s *MusicService) GetTrackFullReleases(trackID string) ([]*model.Release, error) {
|
|
||||||
releases, err := s.GetTrackReleases(trackID)
|
|
||||||
if err != nil { return nil, err }
|
|
||||||
for _, release := range releases {
|
|
||||||
if err := s.fillRelease(release); err != nil { return nil, err }
|
|
||||||
}
|
|
||||||
return releases, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) CreateTrack(title string, description string, lyrics string, previewURL string) (string, error) {
|
|
||||||
if len(title) == 0 { return "", errors.NewValidationError("Track title cannot be empty") }
|
|
||||||
id, err := s.repo.CreateTrack(title, description, lyrics, previewURL)
|
|
||||||
if err != nil { return "", err }
|
|
||||||
s.log.Printf("Created track '%s' (%s)", title, id)
|
|
||||||
return id, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) UpdateTrack(track *model.Track) error {
|
|
||||||
if len(track.Title) == 0 { return errors.NewValidationError("Track title cannot be empty") }
|
|
||||||
if err := s.repo.UpdateTrack(track); err != nil { return err }
|
|
||||||
s.log.Printf("Updated track '%s' (%s)", track.Title, track.ID)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateTrackTitle(id string, title string) error {
|
|
||||||
if len(title) == 0 { return errors.NewValidationError("Track title cannot be empty") }
|
|
||||||
if err := s.repo.UpdateTrackTitle(id, title); err != nil { return err }
|
|
||||||
s.log.Printf("Updated track %s title to '%s'", id, title)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateTrackDescription(id string, description string) error {
|
|
||||||
if err := s.repo.UpdateTrackDescription(id, description); err != nil { return err }
|
|
||||||
s.log.Printf("Updated track %s description to '%s'", id, description)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateTrackLyrics(id string, lyrics string) error {
|
|
||||||
if err := s.repo.UpdateTrackLyrics(id, lyrics); err != nil { return err }
|
|
||||||
s.log.Printf("Updated track %s lyrics to '%s'", id, lyrics)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *MusicService) UpdateTrackPreviewURL(id string, previewURL string) error {
|
|
||||||
if err := s.repo.UpdateTrackPreviewURL(id, previewURL); err != nil { return err }
|
|
||||||
s.log.Printf("Updated track %s preview URL to '%s'", id, previewURL)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *MusicService) DeleteTrack(id string) error {
|
|
||||||
deletedID, err := s.repo.DeleteTrack(id)
|
|
||||||
if err != nil { return err }
|
|
||||||
if deletedID == "" { return errors.NewNotExistError("Track does not exist") }
|
|
||||||
s.log.Printf("Deleted track %s", id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
package validator
|
|
||||||
|
|
||||||
import "regexp"
|
|
||||||
|
|
||||||
var idRegexp = regexp.MustCompile(`^[a-zA-Z0-9\-_\.]+$`)
|
|
||||||
func ValidateID(id string) bool {
|
|
||||||
return idRegexp.MatchString(id)
|
|
||||||
}
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
package validator_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"arimelody-web/service/validator"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"gotest.tools/v3/assert"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_ValidateID(t *testing.T) {
|
|
||||||
t.Run("accepts alphanumberic ID", func(t *testing.T) {
|
|
||||||
assert.Equal(t, validator.ValidateID("abcDEF123"), true)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("accepts hyphens", func(t *testing.T) {
|
|
||||||
assert.Equal(t, validator.ValidateID("a0-1b"), true)
|
|
||||||
})
|
|
||||||
t.Run("accepts underscores", func(t *testing.T) {
|
|
||||||
assert.Equal(t, validator.ValidateID("a0_1b"), true)
|
|
||||||
})
|
|
||||||
t.Run("accepts periods", func(t *testing.T) {
|
|
||||||
assert.Equal(t, validator.ValidateID("a0.1b"), true)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("rejects other characters", func(t *testing.T) {
|
|
||||||
t.Run("emoji", func(t *testing.T) {
|
|
||||||
assert.Equal(t, validator.ValidateID("🗣️🔥‼️"), false)
|
|
||||||
})
|
|
||||||
t.Run("slashes", func(t *testing.T) {
|
|
||||||
assert.Equal(t, validator.ValidateID("this/could/be/really/bad/for/the/router"), false)
|
|
||||||
})
|
|
||||||
t.Run("question marks", func(t *testing.T) {
|
|
||||||
assert.Equal(t, validator.ValidateID("query?these=nuts"), false)
|
|
||||||
})
|
|
||||||
t.Run("hashes", func(t *testing.T) {
|
|
||||||
assert.Equal(t, validator.ValidateID("unnecessary#tagging"), false)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -83,10 +83,9 @@
|
||||||
|
|
||||||
{{else if .IsSingle}}
|
{{else if .IsSingle}}
|
||||||
|
|
||||||
{{with index .Tracks 0}}
|
{{$Track := index .Tracks 0}}
|
||||||
{{if .Description}}
|
{{if $Track.Description}}
|
||||||
<p id="description">{{.Description}}</p>
|
<p id="description">{{$Track.Description}}</p>
|
||||||
{{end}}
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
@ -133,18 +132,18 @@
|
||||||
{{else if .Tracks}}
|
{{else if .Tracks}}
|
||||||
<div id="tracks">
|
<div id="tracks">
|
||||||
<h2>TRACKS</h2>
|
<h2>TRACKS</h2>
|
||||||
{{range .Tracks}}
|
{{range $i, $track := .Tracks}}
|
||||||
<details>
|
<details>
|
||||||
<summary class="album-track-title">{{.Number}}. {{.Title}}</summary>
|
<summary class="album-track-title">{{$track.Add $i 1}}. {{$track.Title}}</summary>
|
||||||
|
|
||||||
{{if .Description}}
|
{{if $track.Description}}
|
||||||
<p class="album-track-subheading">DESCRIPTION</p>
|
<p class="album-track-subheading">DESCRIPTION</p>
|
||||||
{{.Description}}
|
{{$track.Description}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
<p class="album-track-subheading">LYRICS</p>
|
<p class="album-track-subheading">LYRICS</p>
|
||||||
{{if .Lyrics}}
|
{{if $track.Lyrics}}
|
||||||
{{.GetLyricsHTML}}
|
{{$track.GetLyricsHTML}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<span class="empty">No lyrics.</span>
|
<span class="empty">No lyrics.</span>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,14 @@ package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"arimelody-web/controller"
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/twitch"
|
|
||||||
"arimelody-web/templates"
|
"arimelody-web/templates"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
func IndexHandler(app *app.AppState) http.Handler {
|
func IndexHandler(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodHead {
|
if r.Method == http.MethodHead {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
|
|
@ -19,10 +18,10 @@ func IndexHandler(app *app.AppState) http.Handler {
|
||||||
|
|
||||||
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
if r.URL.Path == "/" || r.URL.Path == "/index.html" {
|
||||||
type IndexData struct {
|
type IndexData struct {
|
||||||
TwitchStatus *twitch.StreamInfo
|
TwitchStatus *model.TwitchStreamInfo
|
||||||
}
|
}
|
||||||
var err error
|
var err error
|
||||||
var twitchStatus *twitch.StreamInfo = nil
|
var twitchStatus *model.TwitchStreamInfo = nil
|
||||||
if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 {
|
if app.Twitch != nil && len(app.Config.Twitch.Broadcaster) > 0 {
|
||||||
twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster)
|
twitchStatus, err = controller.GetTwitchStatus(app, app.Config.Twitch.Broadcaster)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
package view
|
package view
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"arimelody-web/controller"
|
"arimelody-web/controller"
|
||||||
"arimelody-web/model"
|
"arimelody-web/model"
|
||||||
"arimelody-web/model/app"
|
"arimelody-web/templates"
|
||||||
"arimelody-web/templates"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTP HANDLER METHODS
|
// HTTP HANDLER METHODS
|
||||||
|
|
||||||
func MusicHandler(app *app.AppState) http.Handler {
|
func MusicHandler(app *model.AppState) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|
||||||
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
@ -22,7 +21,7 @@ func MusicHandler(app *app.AppState) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
release, err := app.MusicService.GetFullReleaseByID(r.URL.Path[1:])
|
release, err := controller.GetRelease(app.DB, r.URL.Path[1:], true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
|
|
@ -34,9 +33,9 @@ func MusicHandler(app *app.AppState) http.Handler {
|
||||||
return mux
|
return mux
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeCatalog(app *app.AppState) http.Handler {
|
func ServeCatalog(app *model.AppState) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
releases, err := app.MusicService.GetAllFullReleases(true, 0)
|
releases, err := controller.GetAllReleases(app.DB, true, 0, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("WARN: Failed to pull releases for catalog: %s\n", err)
|
fmt.Printf("WARN: Failed to pull releases for catalog: %s\n", err)
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
@ -56,7 +55,7 @@ func ServeCatalog(app *app.AppState) http.Handler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func ServeGateway(app *app.AppState, release *model.Release) http.Handler {
|
func ServeGateway(app *model.AppState, release *model.Release) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
// only allow authorised users to view hidden releases
|
// only allow authorised users to view hidden releases
|
||||||
privileged := false
|
privileged := false
|
||||||
|
|
@ -79,15 +78,15 @@ func ServeGateway(app *app.AppState, release *model.Release) http.Handler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !release.IsReleased() && !privileged {
|
response := *release
|
||||||
release.Tracks = nil
|
|
||||||
release.Credits = nil
|
if release.IsReleased() || privileged {
|
||||||
release.Links = nil
|
response.Tracks = release.Tracks
|
||||||
|
response.Credits = release.Credits
|
||||||
|
response.Links = release.Links
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, track := range release.Tracks { track.Number = i + 1 }
|
err := templates.MusicGatewayTemplate.Execute(w, response)
|
||||||
|
|
||||||
err := templates.MusicGatewayTemplate.Execute(w, release)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err)
|
fmt.Printf("Error rendering music gateway for %s: %s\n", release.ID, err)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue