Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LB-1399: Choose specific release in manual MBID mapping #2931

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion admin/timescale/create_indexes.sql
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ CREATE UNIQUE INDEX similar_artist_credit_mbids_reverse_uniq_idx ON similarity.a

CREATE INDEX similarity_overhyped_artists_artist_mbid_idx ON similarity.overhyped_artists(artist_mbid) INCLUDE (factor);

CREATE INDEX mbid_manual_mapping_top_idx ON mbid_manual_mapping_top (recording_msid) INCLUDE (recording_mbid);
CREATE INDEX mbid_manual_mapping_top_idx ON mbid_manual_mapping_top (recording_msid) INCLUDE (recording_mbid, release_mbid);

CREATE INDEX popularity_recording_listen_count_idx ON popularity.recording (total_listen_count) INCLUDE (recording_mbid);
CREATE INDEX popularity_recording_user_count_idx ON popularity.recording (total_user_count) INCLUDE (recording_mbid);
Expand Down
1 change: 1 addition & 0 deletions admin/timescale/create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ CREATE TABLE mbid_manual_mapping(
id INTEGER GENERATED ALWAYS AS IDENTITY NOT NULL,
recording_msid UUID NOT NULL,
recording_mbid UUID NOT NULL,
release_mbid UUID NOT NULL,
user_id INTEGER NOT NULL,
created TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL
);
Expand Down
2 changes: 2 additions & 0 deletions admin/timescale/create_views.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ CREATE MATERIALIZED VIEW mbid_manual_mapping_top AS (
SELECT DISTINCT ON (recording_msid)
recording_msid
, recording_mbid
, release_mbid
FROM mbid_manual_mapping
GROUP BY recording_msid
, recording_mbid
, release_mbid
HAVING count(DISTINCT user_id) >= 3
ORDER BY recording_msid
, count(*) DESC
Expand Down
29 changes: 29 additions & 0 deletions admin/timescale/updates/2024-07-04-add-release-in-mbid-mapping.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
BEGIN;

ALTER TABLE mbid_manual_mapping ADD COLUMN release_mbid UUID NOT NULL;



DROP MATERIALIZED VIEW mbid_manual_mapping_top;

-- create a materialized view of the top mappings of a recording msid. top mappings are chosen
-- by the highest number of users that have added it. if multiple mappings have same count, break
-- ties by preferring the mapping that was created most recently. to avoid abuse, mappings are only
-- considered for this view if at least 3 separate users created those.
CREATE MATERIALIZED VIEW mbid_manual_mapping_top AS (
SELECT DISTINCT ON (recording_msid)
recording_msid
, recording_mbid
, release_mbid
FROM mbid_manual_mapping
GROUP BY recording_msid
, recording_mbid
, release_mbid
HAVING count(DISTINCT user_id) >= 3
ORDER BY recording_msid
, count(*) DESC
, max(created) DESC
);


COMMIT;
31 changes: 16 additions & 15 deletions frontend/css/search-track.less
Original file line number Diff line number Diff line change
Expand Up @@ -18,25 +18,26 @@
// Options within the dropdown menu
> option, > button {
.track-search-dropdown>a,.track-search-dropdown>button {
display: block;
width: 100%;
outline: inherit;
padding: 5px 20px;
font: inherit;
text-align: left;
background: none;
color: inherit;
border: none;
cursor: pointer;
display: block;
width: 100%;
outline: inherit;
padding: 5px 20px;
font: inherit;
text-align: left;
background: none;
color: inherit;
border: none;
cursor: pointer;

&:hover,
&:focus {
color: #fff;
background-color: #353070;
&:hover,
&:focus {
color: #fff;
background-color: #353070;
}
}
}
}
.track-search:focus-within .track-search-dropdown,
.album-search:focus-within .track-search-dropdown {
display: block; // show when parent container is focused
}
}
143 changes: 121 additions & 22 deletions frontend/js/src/common/listens/MBIDMappingModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import NiceModal, { useModal } from "@ebay/nice-modal-react";
import { faTimesCircle } from "@fortawesome/free-regular-svg-icons";
import {
faChevronDown,
faExchangeAlt,
faInfoCircle,
faQuestionCircle,
Expand All @@ -23,29 +24,22 @@ import {
getRecordingMSID,
getTrackName,
} from "../../utils/utils";
import { getListenFromRecording } from "../../user/components/AddListenModal";
import ReleaseCard from "../../explore/fresh-releases/components/ReleaseCard";

export type MBIDMappingModalProps = {
listenToMap?: Listen;
};

function getListenFromSelectedRecording(
selectedRecordingMetadata?: TrackMetadata
): Listen | undefined {
if (!selectedRecordingMetadata) {
return undefined;
}
return {
listened_at: 0,
track_metadata: selectedRecordingMetadata,
};
}

export default NiceModal.create(({ listenToMap }: MBIDMappingModalProps) => {
const modal = useModal();
const { resolve, visible } = modal;
const [copyTextClickCounter, setCopyTextClickCounter] = React.useState(0);
const [selectedRecording, setSelectedRecording] = React.useState<
TrackMetadata
MusicBrainzRecordingWithReleasesAndRGs
>();
const [selectedRelease, setSelectedRelease] = React.useState<
MusicBrainzRelease & WithReleaseGroup
>();

const closeModal = React.useCallback(() => {
Expand Down Expand Up @@ -92,7 +86,7 @@ export default NiceModal.create(({ listenToMap }: MBIDMappingModalProps) => {
if (!listenToMap || !selectedRecording || !auth_token) {
return;
}
const selectedRecordingToListen = getListenFromSelectedRecording(
const selectedRecordingToListen = getListenFromRecording(
selectedRecording
);
const recordingMBID =
Expand All @@ -104,7 +98,8 @@ export default NiceModal.create(({ listenToMap }: MBIDMappingModalProps) => {
await APIService.submitMBIDMapping(
auth_token,
recordingMSID,
recordingMBID
recordingMBID,
selectedRelease?.id
);
} catch (error) {
handleError(error, "Error while linking listen");
Expand Down Expand Up @@ -133,6 +128,7 @@ export default NiceModal.create(({ listenToMap }: MBIDMappingModalProps) => {
APIService,
selectedRecording,
handleError,
selectedRelease,
]
);

Expand All @@ -143,9 +139,9 @@ export default NiceModal.create(({ listenToMap }: MBIDMappingModalProps) => {
);
}, [listenToMap]);

const listenFromSelectedRecording = getListenFromSelectedRecording(
selectedRecording
);
const listenFromSelectedRecording = selectedRecording
? getListenFromRecording(selectedRecording)
: undefined;

if (!listenToMap) {
return null;
Expand Down Expand Up @@ -251,7 +247,10 @@ export default NiceModal.create(({ listenToMap }: MBIDMappingModalProps) => {
title="Reset"
icon={faTimesCircle}
iconSize="lg"
action={() => setSelectedRecording(undefined)}
action={() => {
setSelectedRecording(undefined);
setSelectedRelease(undefined);
}}
/>
}
/>
Expand All @@ -270,15 +269,115 @@ export default NiceModal.create(({ listenToMap }: MBIDMappingModalProps) => {
) : (
<div className="card listen-card">
<SearchTrackOrMBID
expectedPayload="trackmetadata"
expectedPayload="recording"
key={`${defaultValue}-${copyTextClickCounter}`}
onSelectRecording={(trackMetadata) => {
setSelectedRecording(trackMetadata);
onSelectRecording={(newSelectedRecording) => {
setSelectedRecording(newSelectedRecording);
if (newSelectedRecording.releases?.length === 1) {
setSelectedRelease(newSelectedRecording.releases[0]);
} else {
setSelectedRelease(undefined);
}
}}
defaultValue={defaultValue}
/>
</div>
)}
{selectedRecording && selectedRecording?.releases?.length > 1 && (
<>
<h5
data-toggle="collapse"
data-target={`#collapsible-${selectedRecording.id}`}
aria-controls={`collapsible-${selectedRecording.id}`}
className="header-with-line collapsed"
style={{
gap: "5px",
marginBottom: "0.5em",
alignItems: "center",
cursor: "pointer",
}}
id="select-release"
>
Choose from {selectedRecording?.releases?.length}{" "}
releases&nbsp;
<small>(optional)</small>&nbsp;
<FontAwesomeIcon icon={faChevronDown} size="xs" />
</h5>
<div
className="collapse"
id={`collapsible-${selectedRecording.id}`}
>
<div className="help-block">
Too many choices? See more details{" "}
<a
href={`https://musicbrainz.org/recording/${selectedRecording.id}`}
target="_blank"
rel="noopener noreferrer"
>
on MusicBrainz
</a>
.
</div>
<div className="release-cards-grid">
{selectedRecording.releases.map((release) => {
const releaseGroup = release["release-group"];
return (
<span
data-toggle="collapse"
data-target={`#collapsible-${selectedRecording.id}`}
key={release.id}
>
<ReleaseCard
key={release.id}
onClick={() => {
setSelectedRelease(release);
}}
releaseName={release.title}
releaseDate={release.date}
dateFormatOptions={{
year: "numeric",
month: "short",
}}
releaseMBID={release.id}
releaseGroupMBID={releaseGroup?.id}
artistCreditName={selectedRecording[
"artist-credit"
]
?.map(
(artist) =>
`${artist.name}${artist.joinphrase}`
)
.join("")}
artistCredits={selectedRecording[
"artist-credit"
].map((ac) => ({
artist_credit_name: ac.name,
artist_mbid: ac.artist.id,
join_phrase: ac.joinphrase,
}))}
artistMBIDs={selectedRecording[
"artist-credit"
]?.map((ac) => ac.artist.id)}
releaseTypePrimary={
releaseGroup?.["primary-type"]
}
releaseTypeSecondary={releaseGroup?.[
"secondary-types"
].join("+")}
caaID={null}
caaReleaseMBID={release.id}
showTags={false}
showArtist
showReleaseTitle
showInformation
/>
</span>
);
})}
</div>
</div>
</>
)}
</div>
<div className="modal-footer">
<button
Expand Down
17 changes: 12 additions & 5 deletions frontend/js/src/utils/APIService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1049,19 +1049,26 @@ export default class APIService {
submitMBIDMapping = async (
userToken: string,
recordingMSID: string,
recordingMBID: string
recordingMBID: string,
releaseMBID?: string
): Promise<{ status: string }> => {
const url = `${this.APIBaseURI}/metadata/submit_manual_mapping/`;
const optional: { release_mbid?: string } = {};
if (releaseMBID) {
optional.release_mbid = releaseMBID;
}
const body = {
recording_msid: recordingMSID,
recording_mbid: recordingMBID,
...optional,
};
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Token ${userToken}`,
"Content-Type": "application/json;charset=UTF-8",
},
body: JSON.stringify({
recording_msid: recordingMSID,
recording_mbid: recordingMBID,
}),
body: JSON.stringify(body),
});
await this.checkStatus(response);
return response.json();
Expand Down
1 change: 1 addition & 0 deletions listenbrainz/db/dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@
'id',
'recording_msid',
'recording_mbid',
'release_mbid',
'user_id',
'created',
),
Expand Down
Loading