From 57cc60534d50035e718a96dcc8a2f12189c7f736 Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Sun, 15 Jun 2025 22:26:17 -0400 Subject: [PATCH] feat: mark album as various artists --- Makefile | 3 + client/api/api.ts | 4 + .../modals/{RenameModal.tsx => EditModal.tsx} | 51 +++++++----- .../components/modals/SetVariousArtist.tsx | 79 +++++++++++++++++++ client/app/routes/MediaItems/MediaLayout.tsx | 7 +- db/queries/release.sql | 4 + engine/handlers/merge.go | 43 ++++++++++ engine/routes.go | 1 + 8 files changed, 168 insertions(+), 24 deletions(-) rename client/app/components/modals/{RenameModal.tsx => EditModal.tsx} (58%) create mode 100644 client/app/components/modals/SetVariousArtist.tsx diff --git a/Makefile b/Makefile index 78c1fb0..5167863 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,9 @@ postgres.start: postgres.stop: docker stop koito-db +postgres.remove: + docker stop koito-db && docker rm koito-db + api.debug: KOITO_ALLOWED_HOSTS=* KOITO_LOG_LEVEL=debug KOITO_CONFIG_DIR=test_config_dir KOITO_DATABASE_URL=postgres://postgres:secret@localhost:5432?sslmode=disable go run cmd/api/main.go diff --git a/client/api/api.ts b/client/api/api.ts index 150be81..5a3807f 100644 --- a/client/api/api.ts +++ b/client/api/api.ts @@ -156,6 +156,9 @@ function setPrimaryAlias(type: string, id: number, alias: string): Promise { + return fetch(`/apis/web/v1/album?id=${id}`).then(r => r.json() as Promise) +} function deleteListen(listen: Listen): Promise { const ms = new Date(listen.time).getTime() @@ -191,6 +194,7 @@ export { deleteApiKey, updateApiKeyLabel, deleteListen, + getAlbum, } type Track = { id: number diff --git a/client/app/components/modals/RenameModal.tsx b/client/app/components/modals/EditModal.tsx similarity index 58% rename from client/app/components/modals/RenameModal.tsx rename to client/app/components/modals/EditModal.tsx index 4a53ae6..539bb9a 100644 --- a/client/app/components/modals/RenameModal.tsx +++ b/client/app/components/modals/EditModal.tsx @@ -1,9 +1,10 @@ import { useQuery } from "@tanstack/react-query"; -import { createAlias, deleteAlias, getAliases, setPrimaryAlias, type Alias } from "api/api"; +import { createAlias, deleteAlias, getAliases, getAlbum, setPrimaryAlias, type Album, type Alias } from "api/api"; import { Modal } from "./Modal"; import { AsyncButton } from "../AsyncButton"; import { useEffect, useState } from "react"; import { Trash } from "lucide-react"; +import SetVariousArtists from "./SetVariousArtist"; interface Props { type: string @@ -12,11 +13,12 @@ interface Props { setOpen: Function } -export default function RenameModal({ open, setOpen, type, id }: Props) { +export default function EditModal({ open, setOpen, type, id }: Props) { const [input, setInput] = useState('') const [loading, setLoading ] = useState(false) const [err, setError ] = useState() const [displayData, setDisplayData] = useState([]) + const [variousArtists, setVariousArtists] = useState(false) const { isPending, isError, data, error } = useQuery({ queryKey: [ @@ -38,7 +40,6 @@ export default function RenameModal({ open, setOpen, type, id }: Props) { } }, [data]) - if (isError) { return (

Error: {error.message}

@@ -49,6 +50,7 @@ export default function RenameModal({ open, setOpen, type, id }: Props) {

Loading...

) } + const handleSetPrimary = (alias: string) => { setError(undefined) setLoading(true) @@ -98,26 +100,33 @@ export default function RenameModal({ open, setOpen, type, id }: Props) { return ( setOpen(false)}> -

Alias Manager

-
- {displayData.map((v) => ( -
-
{v.alias} (source: {v.source})
- handleSetPrimary(v.alias)} disabled={v.is_primary}>Set Primary - handleDeleteAlias(v.alias)} confirm disabled={v.is_primary}> +
+
+

Alias Manager

+
+ {displayData.map((v) => ( +
+
{v.alias} (source: {v.source})
+ handleSetPrimary(v.alias)} disabled={v.is_primary}>Set Primary + handleDeleteAlias(v.alias)} confirm disabled={v.is_primary}> +
+ ))} +
+ setInput(e.target.value)} + /> + Submit +
+ {err &&

{err}

}
- ))} -
- setInput(e.target.value)} - /> - Submit
- {err &&

{err}

} + { type.toLowerCase() === "album" && + + }
) diff --git a/client/app/components/modals/SetVariousArtist.tsx b/client/app/components/modals/SetVariousArtist.tsx new file mode 100644 index 0000000..8761b9b --- /dev/null +++ b/client/app/components/modals/SetVariousArtist.tsx @@ -0,0 +1,79 @@ +import { useQuery } from "@tanstack/react-query"; +import { getAlbum } from "api/api"; +import { useEffect, useState } from "react" + +interface Props { + id: number +} + +export default function SetVariousArtists({ id }: Props) { + const [err, setErr] = useState('') + const [va, setVA] = useState(false) + const [success, setSuccess] = useState('') + + const { isPending, isError, data, error } = useQuery({ + queryKey: [ + 'get-album', + { + id: id + }, + ], + queryFn: ({ queryKey }) => { + const params = queryKey[1] as { id: number }; + return getAlbum(params.id); + }, + }); + + useEffect(() => { + if (data) { + setVA(data.is_various_artists) + } + }, [data]) + + if (isError) { + return ( +

Error: {error.message}

+ ) + } + if (isPending) { + return ( +

Loading...

+ ) + } + + const updateVA = (val: boolean) => { + setErr(''); + setSuccess(''); + fetch(`/apis/web/v1/album?id=${id}&is_various_artists=${val}`, { method: 'PATCH' }) + .then(r => { + if (r.ok) { + setSuccess('Successfully updated album'); + } else { + r.json().then(r => setErr(r.error)); + } + }); + } + + return ( +
+

Mark as Various Artists

+
+ + {err &&

{err}

} +
+
+ ) +} \ No newline at end of file diff --git a/client/app/routes/MediaItems/MediaLayout.tsx b/client/app/routes/MediaItems/MediaLayout.tsx index 2503d4b..2dcff0b 100644 --- a/client/app/routes/MediaItems/MediaLayout.tsx +++ b/client/app/routes/MediaItems/MediaLayout.tsx @@ -7,7 +7,8 @@ import { useAppContext } from "~/providers/AppProvider"; import MergeModal from "~/components/modals/MergeModal"; import ImageReplaceModal from "~/components/modals/ImageReplaceModal"; import DeleteModal from "~/components/modals/DeleteModal"; -import RenameModal from "~/components/modals/RenameModal"; +import RenameModal from "~/components/modals/EditModal"; +import EditModal from "~/components/modals/EditModal"; export type MergeFunc = (from: number, to: number, replaceImage: boolean) => Promise export type MergeSearchCleanerFunc = (r: SearchResponse, id: number) => SearchResponse @@ -79,11 +80,11 @@ export default function MediaLayout(props: Props) {
{ user &&
- + - + diff --git a/db/queries/release.sql b/db/queries/release.sql index e90d95e..74c5c0a 100644 --- a/db/queries/release.sql +++ b/db/queries/release.sql @@ -104,6 +104,10 @@ LIMIT $1; UPDATE releases SET musicbrainz_id = $2 WHERE id = $1; +-- name: UpdateReleaseVariousArtists :exec +UPDATE releases SET various_artists = $2 +WHERE id = $1; + -- name: UpdateReleaseImage :exec UPDATE releases SET image = $2, image_source = $3 WHERE id = $1; diff --git a/engine/handlers/merge.go b/engine/handlers/merge.go index 41d38cc..26da665 100644 --- a/engine/handlers/merge.go +++ b/engine/handlers/merge.go @@ -131,3 +131,46 @@ func MergeArtistsHandler(store db.DB) http.HandlerFunc { w.WriteHeader(http.StatusNoContent) } } + +func UpdateAlbumHandler(store db.DB) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + l := logger.FromContext(ctx) + + l.Debug().Msg("UpdateAlbumHandler: Received request") + + idStr := r.URL.Query().Get("id") + id, err := strconv.Atoi(idStr) + + valStr := r.URL.Query().Get("is_various_artists") + var variousArists bool + var updateVariousArtists = false + if strings.ToLower(valStr) == "true" { + variousArists = true + updateVariousArtists = true + } else if strings.ToLower(valStr) == "false" { + variousArists = false + updateVariousArtists = true + } + if err != nil { + l.Debug().AnErr("error", err).Msg("UpdateAlbumHandler: Invalid id parameter") + utils.WriteError(w, "id is invalid", http.StatusBadRequest) + return + } + + err = store.UpdateAlbum(ctx, db.UpdateAlbumOpts{ + ID: int32(id), + VariousArtistsUpdate: updateVariousArtists, + VariousArtistsValue: variousArists, + }) + if err != nil { + l.Debug().AnErr("error", err).Msg("UpdateAlbumHandler: Failed to update album") + utils.WriteError(w, "failed to update album", http.StatusBadRequest) + return + } + + l.Debug().Msg("UpdateAlbumHandler: Successfully updated album") + + w.WriteHeader(http.StatusNoContent) + } +} diff --git a/engine/routes.go b/engine/routes.go index 4b7d302..18fc164 100644 --- a/engine/routes.go +++ b/engine/routes.go @@ -70,6 +70,7 @@ func bindRoutes( r.Group(func(r chi.Router) { r.Use(middleware.ValidateSession(db)) r.Post("/replace-image", handlers.ReplaceImageHandler(db)) + r.Patch("/album", handlers.UpdateAlbumHandler(db)) r.Post("/merge/tracks", handlers.MergeTracksHandler(db)) r.Post("/merge/albums", handlers.MergeReleaseGroupsHandler(db)) r.Post("/merge/artists", handlers.MergeArtistsHandler(db))