mirror of https://github.com/gabehf/Koito.git
parent
00e7782be2
commit
80b6f4deaa
@ -1,5 +1,18 @@
|
|||||||
# v0.0.7
|
# v0.0.8
|
||||||
|
## Features
|
||||||
|
- An album artist can now be set as primary so that they are shown as the album artist in the top albums list
|
||||||
|
|
||||||
|
## Enhancements
|
||||||
|
- Show a few more items under "Last Played" on the home page
|
||||||
|
- Importing is now 4-5x faster
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
- Login form now correctly handles special characters
|
- Merge selections now function correctly when selecting an item while another is selected
|
||||||
- Update User form now correctly handles special characters
|
- Use anchor tags for top tracks and top albums
|
||||||
- Delete Listen button is now hidden when not logged in
|
- UI fixes
|
||||||
|
|
||||||
|
## Updates
|
||||||
|
- Improved logging and error traces in logs
|
||||||
|
|
||||||
|
## Docs
|
||||||
|
- Add KOITO_FETCH_IMAGES_DURING_IMPORT to config reference
|
||||||
@ -0,0 +1,99 @@
|
|||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { getAlbum, type Artist } from "api/api";
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: number
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SetPrimaryArtist({ id, type }: Props) {
|
||||||
|
const [err, setErr] = useState('')
|
||||||
|
const [primary, setPrimary] = useState<Artist>()
|
||||||
|
const [success, setSuccess] = useState('')
|
||||||
|
|
||||||
|
const { isPending, isError, data, error } = useQuery({
|
||||||
|
queryKey: [
|
||||||
|
'get-artists-'+type.toLowerCase(),
|
||||||
|
{
|
||||||
|
id: id
|
||||||
|
},
|
||||||
|
],
|
||||||
|
queryFn: () => {
|
||||||
|
return fetch('/apis/web/v1/artists?'+type.toLowerCase()+'_id='+id).then(r => r.json()) as Promise<Artist[]>;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (data) {
|
||||||
|
for (let a of data) {
|
||||||
|
if (a.is_primary) {
|
||||||
|
setPrimary(a)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [data])
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
return (
|
||||||
|
<p className="error">Error: {error.message}</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (isPending) {
|
||||||
|
return (
|
||||||
|
<p>Loading...</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePrimary = (artist: number, val: boolean) => {
|
||||||
|
setErr('');
|
||||||
|
setSuccess('');
|
||||||
|
fetch(`/apis/web/v1/artists/primary?artist_id=${artist}&${type.toLowerCase()}_id=${id}&is_primary=${val}`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(r => {
|
||||||
|
if (r.ok) {
|
||||||
|
setSuccess('successfully updated primary artists');
|
||||||
|
} else {
|
||||||
|
r.json().then(r => setErr(r.error));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<h2>Set Primary Artist</h2>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<select
|
||||||
|
name="mark-various-artists"
|
||||||
|
id="mark-various-artists"
|
||||||
|
className="w-60 px-3 py-2 rounded-md"
|
||||||
|
value={primary?.name || ""}
|
||||||
|
onChange={(e) => {
|
||||||
|
for (let a of data) {
|
||||||
|
if (a.name === e.target.value) {
|
||||||
|
setPrimary(a);
|
||||||
|
updatePrimary(a.id, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<option value="" disabled>
|
||||||
|
Select an artist
|
||||||
|
</option>
|
||||||
|
{data.map((a) => (
|
||||||
|
<option key={a.id} value={a.name}>
|
||||||
|
{a.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
{err && <p className="error">{err}</p>}
|
||||||
|
{success && <p className="success">{success}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,48 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
SELECT 'up SQL query';
|
||||||
|
-- +goose StatementEnd
|
||||||
|
ALTER TABLE artist_tracks
|
||||||
|
ADD COLUMN is_primary boolean NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
ALTER TABLE artist_releases
|
||||||
|
ADD COLUMN is_primary boolean NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE FUNCTION get_artists_for_release(release_id INTEGER)
|
||||||
|
RETURNS JSONB AS $$
|
||||||
|
SELECT json_agg(
|
||||||
|
jsonb_build_object('id', a.id, 'name', a.name)
|
||||||
|
ORDER BY ar.is_primary DESC, a.name
|
||||||
|
)
|
||||||
|
FROM artist_releases ar
|
||||||
|
JOIN artists_with_name a ON a.id = ar.artist_id
|
||||||
|
WHERE ar.release_id = $1;
|
||||||
|
$$ LANGUAGE sql STABLE;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE FUNCTION get_artists_for_track(track_id INTEGER)
|
||||||
|
RETURNS JSONB AS $$
|
||||||
|
SELECT json_agg(
|
||||||
|
jsonb_build_object('id', a.id, 'name', a.name)
|
||||||
|
ORDER BY at.is_primary DESC, a.name
|
||||||
|
)
|
||||||
|
FROM artist_tracks at
|
||||||
|
JOIN artists_with_name a ON a.id = at.artist_id
|
||||||
|
WHERE at.track_id = $1;
|
||||||
|
$$ LANGUAGE sql STABLE;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
SELECT 'down SQL query';
|
||||||
|
-- +goose StatementEnd
|
||||||
|
ALTER TABLE artist_tracks
|
||||||
|
DROP COLUMN is_primary;
|
||||||
|
|
||||||
|
ALTER TABLE artist_releases
|
||||||
|
DROP COLUMN is_primary;
|
||||||
|
|
||||||
|
DROP FUNCTION IF EXISTS get_artists_for_release(INTEGER);
|
||||||
|
DROP FUNCTION IF EXISTS get_artists_for_track(INTEGER);
|
||||||
@ -0,0 +1,156 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gabehf/koito/internal/db"
|
||||||
|
"github.com/gabehf/koito/internal/logger"
|
||||||
|
"github.com/gabehf/koito/internal/models"
|
||||||
|
"github.com/gabehf/koito/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SetPrimaryArtistHandler(store db.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
// sets the primary alias for albums, artists, and tracks
|
||||||
|
ctx := r.Context()
|
||||||
|
l := logger.FromContext(ctx)
|
||||||
|
|
||||||
|
l.Debug().Msg("SetPrimaryArtistHandler: Got request")
|
||||||
|
|
||||||
|
r.ParseForm()
|
||||||
|
|
||||||
|
// Parse query parameters
|
||||||
|
artistIDStr := r.FormValue("artist_id")
|
||||||
|
albumIDStr := r.FormValue("album_id")
|
||||||
|
trackIDStr := r.FormValue("track_id")
|
||||||
|
isPrimaryStr := r.FormValue("is_primary")
|
||||||
|
|
||||||
|
l.Debug().Str("query", r.Form.Encode()).Msg("Recieved form")
|
||||||
|
|
||||||
|
if artistIDStr == "" {
|
||||||
|
l.Debug().Msg("SetPrimaryArtistHandler: artist_id must be provided")
|
||||||
|
utils.WriteError(w, "artist_id must be provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isPrimaryStr == "" {
|
||||||
|
l.Debug().Msg("SetPrimaryArtistHandler: is_primary must be provided")
|
||||||
|
utils.WriteError(w, "is_primary must be provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
primary, ok := utils.ParseBool(isPrimaryStr)
|
||||||
|
if !ok {
|
||||||
|
l.Debug().Msg("SetPrimaryArtistHandler: is_primary must be either true or false")
|
||||||
|
utils.WriteError(w, "is_primary must be either true or false", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
artistId, err := strconv.Atoi(artistIDStr)
|
||||||
|
if err != nil {
|
||||||
|
l.Debug().Msg("SetPrimaryArtistHandler: artist_id is invalid")
|
||||||
|
utils.WriteError(w, "artist_id is invalid", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if albumIDStr == "" && trackIDStr == "" {
|
||||||
|
l.Debug().Msg("SetPrimaryArtistHandler: Missing album or track id parameter")
|
||||||
|
utils.WriteError(w, "album_id or track_id must be provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if utils.MoreThanOneString(albumIDStr, trackIDStr) {
|
||||||
|
l.Debug().Msg("SetPrimaryArtistHandler: Multiple ID parameters provided")
|
||||||
|
utils.WriteError(w, "only one of album_id or track_id can be provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if albumIDStr != "" {
|
||||||
|
id, err := strconv.Atoi(albumIDStr)
|
||||||
|
if err != nil {
|
||||||
|
l.Debug().AnErr("error", err).Msg("SetPrimaryArtistHandler: Invalid album id")
|
||||||
|
utils.WriteError(w, "invalid album_id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = store.SetPrimaryAlbumArtist(ctx, int32(id), int32(artistId), primary)
|
||||||
|
if err != nil {
|
||||||
|
l.Error().Err(err).Msg("SetPrimaryArtistHandler: Failed to set album primary alias")
|
||||||
|
utils.WriteError(w, "failed to set primary alias", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if trackIDStr != "" {
|
||||||
|
id, err := strconv.Atoi(trackIDStr)
|
||||||
|
if err != nil {
|
||||||
|
l.Debug().AnErr("error", err).Msg("SetPrimaryArtistHandler: Invalid track id")
|
||||||
|
utils.WriteError(w, "invalid track_id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = store.SetPrimaryTrackArtist(ctx, int32(id), int32(artistId), primary)
|
||||||
|
if err != nil {
|
||||||
|
l.Error().Err(err).Msg("SetPrimaryArtistHandler: Failed to set track primary alias")
|
||||||
|
utils.WriteError(w, "failed to set primary alias", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func GetArtistsForItemHandler(store db.DB) http.HandlerFunc {
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
l := logger.FromContext(ctx)
|
||||||
|
|
||||||
|
l.Debug().Msg("GetArtistsForItemHandler: Received request to retrieve artists for item")
|
||||||
|
|
||||||
|
albumIDStr := r.URL.Query().Get("album_id")
|
||||||
|
trackIDStr := r.URL.Query().Get("track_id")
|
||||||
|
|
||||||
|
if albumIDStr == "" && trackIDStr == "" {
|
||||||
|
l.Debug().Msg("GetArtistsForItemHandler: Missing album or track ID parameter")
|
||||||
|
utils.WriteError(w, "album_id or track_id must be provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if utils.MoreThanOneString(albumIDStr, trackIDStr) {
|
||||||
|
l.Debug().Msg("GetArtistsForItemHandler: Multiple ID parameters provided")
|
||||||
|
utils.WriteError(w, "only one of album_id or track_id can be provided", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var artists []*models.Artist
|
||||||
|
var err error
|
||||||
|
|
||||||
|
if albumIDStr != "" {
|
||||||
|
albumID, convErr := strconv.Atoi(albumIDStr)
|
||||||
|
if convErr != nil {
|
||||||
|
l.Debug().AnErr("error", convErr).Msg("GetArtistsForItemHandler: Invalid album ID")
|
||||||
|
utils.WriteError(w, "invalid album_id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug().Msgf("GetArtistsForItemHandler: Fetching artists for album ID %d", albumID)
|
||||||
|
artists, err = store.GetArtistsForAlbum(ctx, int32(albumID))
|
||||||
|
} else if trackIDStr != "" {
|
||||||
|
trackID, convErr := strconv.Atoi(trackIDStr)
|
||||||
|
if convErr != nil {
|
||||||
|
l.Debug().AnErr("error", convErr).Msg("GetArtistsForItemHandler: Invalid track ID")
|
||||||
|
utils.WriteError(w, "invalid track_id", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug().Msgf("GetArtistsForItemHandler: Fetching artists for track ID %d", trackID)
|
||||||
|
artists, err = store.GetArtistsForTrack(ctx, int32(trackID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
l.Err(err).Msg("GetArtistsForItemHandler: Failed to retrieve artists")
|
||||||
|
utils.WriteError(w, "failed to retrieve artists", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Debug().Msg("GetArtistsForItemHandler: Successfully retrieved artists")
|
||||||
|
utils.WriteJSON(w, http.StatusOK, artists)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue