Skip to content

Commit

Permalink
Merge pull request #3062 from metabrainz/LB-1639
Browse files Browse the repository at this point in the history
LB-1639: Rename "missing data" to "link listens" and add it to explore page
  • Loading branch information
anshg1214 authored Dec 13, 2024
2 parents 20f2630 + 26086e6 commit 62b8a87
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 64 deletions.
Binary file added frontend/img/explore/link-listens.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions frontend/js/src/explore/Explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ export default function ExplorePage() {
url="/explore/fresh-releases/"
/>
</div>
<div>
<ExploreCard
name="Link listens"
desc="Fix your unlinked listens"
img_name="link-listens.jpg"
url="/settings/link-listens/"
/>
</div>
<div>
<ExploreCard
name="Hue Sound"
Expand Down
2 changes: 1 addition & 1 deletion frontend/js/src/settings/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const sections: Section[] = [
{ to: "music-services/details/", label: "Connect services" },
{ to: "brainzplayer/", label: "Music player" },
{ to: "import/", label: "Import listens" },
{ to: "missing-data/", label: "Missing data" },
{ to: "link-listens/", label: "Link listens" },
],
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@

import * as React from "react";

import { faLink, faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import {
faLink,
faQuestionCircle,
faTrashAlt,
} from "@fortawesome/free-solid-svg-icons";
import { Link, useLocation, useSearchParams } from "react-router-dom";
import { toast } from "react-toastify";
import { Helmet } from "react-helmet";
Expand All @@ -12,6 +16,7 @@ import NiceModal from "@ebay/nice-modal-react";
import { groupBy, isNil, isNull, pick, size, sortBy } from "lodash";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { useQuery } from "@tanstack/react-query";
import ReactTooltip from "react-tooltip";
import Loader from "../../components/Loader";
import ListenCard from "../../common/listens/ListenCard";
import ListenControl from "../../common/listens/ListenControl";
Expand All @@ -26,26 +31,26 @@ import Accordion from "../../common/Accordion";
import { useBrainzPlayerDispatch } from "../../common/brainzplayer/BrainzPlayerContext";
import { RouteQuery } from "../../utils/Loader";

export type MissingMBDataProps = {
missingData?: Array<MissingMBData>;
export type LinkListensProps = {
unlinkedListens?: Array<UnlinkedListens>;
user: ListenBrainzUser;
};

type MissingMBDataLoaderData = {
missing_data?: Array<MissingMBData>;
type LinkListensLoaderData = {
unlinked_listens?: Array<UnlinkedListens>;
last_updated?: string | null;
};

export interface MissingMBDataState {
missingData: Array<MissingMBData>;
groupedMissingData: Array<MissingMBData[]>;
export interface LinkListensState {
unlinkedListens: Array<UnlinkedListens>;
groupedUnlinkedListens: Array<UnlinkedListens[]>;
deletedListens: Array<string>; // array of recording_msid of deleted items
currPage: number;
loading: boolean;
}

export function missingDataToListen(
data: MissingMBData,
export function unlinkedListenDataToListen(
data: UnlinkedListens,
user: ListenBrainzUser
): Listen {
return {
Expand All @@ -64,59 +69,64 @@ export function missingDataToListen(

const EXPECTED_ITEMS_PER_PAGE = 25;

export default function MissingMBDataPage() {
export default function LinkListensPage() {
// Context
const { APIService, currentUser: user } = React.useContext(GlobalAppContext);
const dispatch = useBrainzPlayerDispatch();
const location = useLocation();
// Loader
const { data: loaderData, isLoading } = useQuery<MissingMBDataLoaderData>(
RouteQuery(["missing-data"], location.pathname)
const { data: loaderData, isLoading } = useQuery<LinkListensLoaderData>(
RouteQuery(["link-listens"], location.pathname)
);
const { missing_data: missingDataProps = [], last_updated: lastUpdated } =
loaderData || {};
const {
unlinked_listens: unlinkedListensProps = [],
last_updated: lastUpdated,
} = loaderData || {};

const [searchParams, setSearchParams] = useSearchParams();
const pageSearchParam = searchParams.get("page");

// State
const [deletedListens, setDeletedListens] = React.useState<Array<string>>([]);
const [missingData, setMissingData] = React.useState<Array<MissingMBData>>(
missingDataProps
const [unlinkedListens, setUnlinkedListens] = React.useState<
Array<UnlinkedListens>
>(unlinkedListensProps);
const unsortedGroupedUnlinkedListens = groupBy(
unlinkedListens,
"release_name"
);
const unsortedGroupedMissingData = groupBy(missingData, "release_name");
// remove and store a catchall group with no release name
const noReleaseNameGroup = pick(unsortedGroupedMissingData, "null");
const noReleaseNameGroup = pick(unsortedGroupedUnlinkedListens, "null");
if (size(noReleaseNameGroup) > 0) {
// remove catchall group from other groups,
// we want to add it at the very end
delete unsortedGroupedMissingData.null;
delete unsortedGroupedUnlinkedListens.null;
}
const sortedMissingDataGroups = sortBy(
unsortedGroupedMissingData,
const sortedUnlinkedListensGroups = sortBy(
unsortedGroupedUnlinkedListens,
"length"
).reverse();
if (noReleaseNameGroup.null?.length) {
// re-add the group with no release name at the end,
// will be displayed as single listens rather than a group
sortedMissingDataGroups.push(noReleaseNameGroup.null);
sortedUnlinkedListensGroups.push(noReleaseNameGroup.null);
}

// Pagination
const currPage = isNull(pageSearchParam) ? 1 : parseInt(pageSearchParam, 10);
const totalPages = unsortedGroupedMissingData
? Math.ceil(size(unsortedGroupedMissingData) / EXPECTED_ITEMS_PER_PAGE)
const totalPages = unsortedGroupedUnlinkedListens
? Math.ceil(size(unsortedGroupedUnlinkedListens) / EXPECTED_ITEMS_PER_PAGE)
: 0;

const offset = (currPage - 1) * EXPECTED_ITEMS_PER_PAGE;
const itemsOnThisPage = sortedMissingDataGroups.slice(
const itemsOnThisPage = sortedUnlinkedListensGroups.slice(
offset,
offset + EXPECTED_ITEMS_PER_PAGE
);

// Functions

const deleteListen = async (data: MissingMBData) => {
const deleteListen = async (data: UnlinkedListens) => {
if (user?.auth_token) {
const listenedAt = new Date(data.listened_at).getTime() / 1000;
try {
Expand All @@ -143,7 +153,7 @@ export default function MissingMBDataPage() {
dispatch({
type: "REMOVE_TRACK_FROM_AMBIENT_QUEUE",
data: {
track: missingDataToListen(data, user),
track: unlinkedListenDataToListen(data, user),
index: -1,
},
});
Expand Down Expand Up @@ -178,40 +188,58 @@ export default function MissingMBDataPage() {

// BrainzPlayer
React.useEffect(() => {
const missingMBDataAsListen = itemsOnThisPage.flatMap((x) => [
...x.map((y) => missingDataToListen(y, user)),
const unlinkedDataAsListen = itemsOnThisPage.flatMap((x) => [
...x.map((y) => unlinkedListenDataToListen(y, user)),
]);
dispatch({
type: "SET_AMBIENT_QUEUE",
data: missingMBDataAsListen,
data: unlinkedDataAsListen,
});
}, [dispatch, itemsOnThisPage, user]);

return (
<>
<Helmet>
<title>Missing MusicBrainz Data of {user?.name}</title>
<title>Link with MusicBrainz</title>
</Helmet>
<h2 className="page-title">Missing MusicBrainz Data of {user?.name}</h2>
<h2 className="page-title">Link with MusicBrainz</h2>
<ReactTooltip id="matching-tooltip" multiline>
We automatically match listens with MusicBrainz recordings when
possible, which provides rich data like tags, album, artists, cover art,
and more.
<br />
When a track can&apos;t be auto-matched you can manually link them on
this page.
<br />
Recordings may not exist in MusicBrainz, and need to be added there
first.
</ReactTooltip>
<p>
Your top 1000 listens that haven&apos;t been automatically linked. Link
the listens below, or&nbsp;
You will find below your top 1000 listens (grouped by album) that have
not been automatically linked
<FontAwesomeIcon
icon={faQuestionCircle}
size="sm"
data-tip
data-for="matching-tooltip"
/>{" "}
to a MusicBrainz recording. Link them below or&nbsp;
<a href="https://wiki.musicbrainz.org/How_to_Contribute">
submit new data to MusicBrainz
</a>
.
</p>
<p>
<p className="small">
<a href="https://musicbrainz.org/">MusicBrainz</a> is the open-source
music encyclopedia that ListenBrainz uses to display information about
your music.
music encyclopedia that ListenBrainz uses to display more information
about your music.
</p>
{!isNil(lastUpdated) && (
<p>Last updated {new Date(lastUpdated).toLocaleDateString()}</p>
)}
<br />
<div>
<div id="missingMBData">
<div id="link-listens">
<div
style={{
height: 0,
Expand All @@ -233,7 +261,7 @@ export default function MissingMBDataPage() {
NiceModal.show<MatchingTracksResults, any>(
MultiTrackMBIDMappingModal,
{
missingData: group,
unlinkedListens: group,
releaseName,
}
).then((matchedTracks) => {
Expand All @@ -250,7 +278,7 @@ export default function MissingMBDataPage() {
dispatch({
type: "REMOVE_TRACK_FROM_AMBIENT_QUEUE",
data: {
track: missingDataToListen(
track: unlinkedListenDataToListen(
itemBeforeMatching,
user
),
Expand All @@ -261,7 +289,7 @@ export default function MissingMBDataPage() {
}
);
// Remove successfully matched items from the page
setMissingData((prevValue) =>
setUnlinkedListens((prevValue) =>
prevValue.filter(
(md) => !matchedTracks[md.recording_msid]
)
Expand All @@ -284,7 +312,7 @@ export default function MissingMBDataPage() {
return undefined;
}
let additionalActions;
const listen = missingDataToListen(groupItem, user);
const listen = unlinkedListenDataToListen(groupItem, user);
const additionalMenuItems = [];
if (user?.auth_token) {
const recordingMSID = getRecordingMSID(listen);
Expand Down Expand Up @@ -323,7 +351,7 @@ export default function MissingMBDataPage() {
},
});
// Remove successfully matched item from the page
setMissingData((prevValue) =>
setUnlinkedListens((prevValue) =>
prevValue.filter(
(md) =>
md.recording_msid !==
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export type MatchingTracksResults = {

export type MultiTrackMBIDMappingModalProps = {
releaseName: string | null;
missingData: Array<MissingMBData>;
missingData: Array<UnlinkedListens>;
};

// https://lucene.apache.org/core/7_7_2/queryparser/org/apache/lucene/queryparser/classic/package-summary.html#Escaping_Special_Characters
Expand Down
8 changes: 4 additions & 4 deletions frontend/js/src/settings/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,11 @@ const getSettingsRoutes = (): RouteObject[] => {
},
},
{
path: "missing-data/",
loader: RouteQueryLoader("missing-data"),
path: "link-listens/",
loader: RouteQueryLoader("link-listens"),
lazy: async () => {
const MissingMBData = await import("../missing-data/MissingMBData");
return { Component: MissingMBData.default };
const LinkListens = await import("../link-listens/LinkListens");
return { Component: LinkListens.default };
},
},
{
Expand Down
4 changes: 2 additions & 2 deletions frontend/js/src/settings/routes/redirectRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ const getRedirectRoutes = (): RouteObject[] => {
element: <Navigate to="/settings/import/" replace />,
},
{
path: "missing-data/",
element: <Navigate to="/settings/missing-data/" replace />,
path: "link-listens/",
element: <Navigate to="/settings/link-listens/" replace />,
},
{
path: "select_timezone/",
Expand Down
2 changes: 1 addition & 1 deletion frontend/js/src/utils/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,7 +612,7 @@ type ColorReleasesResponse = {
};
};

type MissingMBData = {
type UnlinkedListens = {
artist_name: string;
listened_at: string;
recording_name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const user = userEvent.setup();
const routes = getSettingsRoutes();

const pageData = {
missing_data: missingDataProps.missingData,
unlinked_listens: missingDataProps.missingData,
};
// React-Query setup
const queryClient = new QueryClient({
Expand All @@ -30,21 +30,21 @@ const queryClient = new QueryClient({
},
},
});
const queryKey = ["missing-data"];
const queryKey = ["link-listens"];

const reactQueryWrapper = ({ children }: any) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);

describe("MissingMBDataPage", () => {
describe("LinkListensPage", () => {
let server: SetupServerApi;
let router: Router;
beforeAll(async () => {
window.scrollTo = jest.fn();
window.HTMLElement.prototype.scrollIntoView = jest.fn();
// Mock the server responses
const handlers = [
http.post("/settings/missing-data/", ({ request }) => {
http.post("/settings/link-listens/", ({ request }) => {
return HttpResponse.json(pageData);
}),
];
Expand All @@ -53,7 +53,7 @@ describe("MissingMBDataPage", () => {
// Create the router *after* MSW mock server is set up
// See https://github.com/mswjs/msw/issues/1653#issuecomment-1781867559
router = createMemoryRouter(routes, {
initialEntries: ["/settings/missing-data/"],
initialEntries: ["/settings/link-listens/"],
});
});
beforeEach(async () => {
Expand All @@ -80,7 +80,7 @@ describe("MissingMBDataPage", () => {
false
);
await screen.findByText(
textContentMatcher("Missing MusicBrainz Data of riksucks")
textContentMatcher("Link with MusicBrainz")
);
const albumGroups = await screen.findAllByRole("heading", { level: 3 });
// 25 groups per page
Expand Down
Loading

0 comments on commit 62b8a87

Please sign in to comment.