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
|
||||
- Login form now correctly handles special characters
|
||||
- Update User form now correctly handles special characters
|
||||
- Delete Listen button is now hidden when not logged in
|
||||
- Merge selections now function correctly when selecting an item while another is selected
|
||||
- Use anchor tags for top tracks and top albums
|
||||
- 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