[musicbrainz] Add advanced lookup support for covers

This commit is contained in:
SuperSaltyGamer
2023-08-30 00:01:26 +03:00
parent 55356e8174
commit 71237a428e
8 changed files with 203 additions and 52 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
// ==UserScript==
// @namespace ame-musicbrainz
// @name Ame (MusicBrainz)
// @version 1.5.0
// @version 1.6.0
// @author SuperSaltyGamer
// @run-at document-end
// @match https://musicbrainz.org/*

View File

@@ -1,7 +1,14 @@
import { fetchCors } from "../../common/fetch";
import { fromHTML } from "../../common/dom";
import { onReleaseAddCoverRoute } from "../glue/router";
import { getPageReleaseInfo } from "../services/release";
import { ReleaseInfo, TocType, getPageReleaseInfo, getReleaseToc } from "../services/release";
enum QueryType {
Search = "search",
Barcode = "barcode",
Catalog = "catalog",
Toc = "toc"
}
interface CoverData {
action: string;
@@ -17,24 +24,52 @@ onReleaseAddCoverRoute(() => {
const refEl = document.querySelector(".fileinput-button.buttons");
if (!refEl) return;
const buttonEl = fromHTML(`<button type="button">Pick from MH Covers...</button>`);
buttonEl.onclick = openPicker;
const release = getPageReleaseInfo();
if (!release) return;
const buttonEl = fromHTML<HTMLSelectElement>(`
<select>
<option disabled selected>Search MH Covers...</option>
<option value="search">by Artist and Album</option>
${release.barcode ? `<option value="barcode">by Barcode</option>` : ""}
${release.catalogs.length ? `<option value="catalog">by Catalog</option>` : ""}
${release.tocType === TocType.Exact || release.tocType === TocType.Deduced ? `<option value="toc">by TOC</option>` : ""}
</select>
`);
buttonEl.addEventListener("input", async () => {
await openPicker(release, buttonEl.value as QueryType);
});
refEl.appendChild(buttonEl);
});
function openPicker(e: MouseEvent) {
e.preventDefault();
const release = getPageReleaseInfo();
if (!release) return;
async function openPicker(releaseInfo: ReleaseInfo, queryType: QueryType) {
const params = new URLSearchParams();
params.set("artist", release.artist);
params.set("album", release.title);
params.set("remote.port", "browser");
params.set("remote.agent", "Ame - MusicBrainz");
params.set("remote.text", "Pick cover for MusicBrainz release.");
switch (queryType) {
case QueryType.Search:
params.set("artist", releaseInfo.artist);
params.set("album", releaseInfo.title);
break;
case QueryType.Barcode:
if (!releaseInfo.barcode) return;
params.set("barcode", releaseInfo.barcode);
break;
case QueryType.Catalog:
if (!releaseInfo.catalogs.length) return;
params.set("catalog", releaseInfo.catalogs[0]);
break;
case QueryType.Toc:
const toc = await getReleaseToc(releaseInfo);
if (!toc) return;
params.set("toc", toc);
break;
default:
return;
}
const win = open(`https://covers.musichoarders.xyz?${params}`, "_blank");
if (!win) return;

View File

@@ -1,29 +1,32 @@
import { onReleaseRoute } from "../glue/router";
import { ReleaseInfo, getPageReleaseInfo } from "../services/release";
import { ReleaseInfo, TocType, getPageReleaseInfo, getReleaseToc } from "../services/release";
import { addReleaseSidebarButton } from "../glue/sidebar";
import mhCoversIcon from "../assets/icons/mhcovers.svg";
import ongakuNoMoriIcon from "../assets/icons/ongakunomori.ico";
onReleaseRoute(async () => {
const release = getPageReleaseInfo();
if (!release) return;
const releaseInfo = getPageReleaseInfo();
if (!releaseInfo) return;
await Promise.all([
addOngakuNoMoriToRelease(release),
addMhCoversToRelease(release)
addOngakuNoMoriToRelease(releaseInfo),
addMhCoversToRelease(releaseInfo)
]);
});
function addOngakuNoMoriToRelease(release: ReleaseInfo) {
const dn = release.barcode ?? release.catalogs[0];
function addOngakuNoMoriToRelease(releaseInfo: ReleaseInfo) {
const dn = releaseInfo.barcode ?? releaseInfo.catalogs[0];
if (!dn) return;
addReleaseSidebarButton(200, ongakuNoMoriIcon, "音楽の森 <small>(Search)</small>", `https://search.minc.or.jp/product/list/?type=search-form-diskno&dn=${dn}`);
}
function addMhCoversToRelease(release: ReleaseInfo) {
addReleaseSidebarButton(300, mhCoversIcon, "MH Covers <small>(Search)</small>", `https://covers.musichoarders.xyz?artist=${encodeURIComponent(release.artist)}&album=${encodeURIComponent(release.title)}`);
if (release.tocs.length && release.tocs[0].split(':').length - 1 >= 4) addReleaseSidebarButton(400, mhCoversIcon, "MH Covers <small>(Search by TOC)</small>", `https://covers.musichoarders.xyz?toc=${encodeURIComponent(release.tocs[0])}`);
if (release.barcode) addReleaseSidebarButton(500, mhCoversIcon, "MH Covers <small>(Search by Barcode)</small>", `https://covers.musichoarders.xyz?barcode=${encodeURIComponent(release.barcode)}`);
if (release.catalogs.length) addReleaseSidebarButton(600, mhCoversIcon, "MH Covers <small>(Search by Catalog)</small>", `https://covers.musichoarders.xyz?catalog=${encodeURIComponent(release.catalogs[0])}`);
async function addMhCoversToRelease(releaseInfo: ReleaseInfo) {
addReleaseSidebarButton(300, mhCoversIcon, "MH Covers <small>(Search)</small>", `https://covers.musichoarders.xyz?artist=${encodeURIComponent(releaseInfo.artist)}&album=${encodeURIComponent(releaseInfo.title)}`);
if (releaseInfo.tocType === TocType.Exact || releaseInfo.tocType === TocType.Deduced) {
const toc = await getReleaseToc(releaseInfo);
if (toc) addReleaseSidebarButton(400, mhCoversIcon, "MH Covers <small>(Search by TOC)</small>", `https://covers.musichoarders.xyz?toc=${encodeURIComponent(toc)}`);
}
if (releaseInfo.barcode) addReleaseSidebarButton(500, mhCoversIcon, "MH Covers <small>(Search by Barcode)</small>", `https://covers.musichoarders.xyz?barcode=${encodeURIComponent(releaseInfo.barcode)}`);
if (releaseInfo.catalogs.length) addReleaseSidebarButton(600, mhCoversIcon, "MH Covers <small>(Search by Catalog)</small>", `https://covers.musichoarders.xyz?catalog=${encodeURIComponent(releaseInfo.catalogs[0])}`);
}

View File

@@ -3,8 +3,8 @@ import { getPageReleaseId } from "../services/release";
enum QueryType {
Unknown = "unknown",
Catalog = "catalog",
Barcode = "barcode",
Catalog = "catalog",
Isrc = "isrc",
Toc = "toc"
}

View File

@@ -1,4 +1,11 @@
import { parseDuration } from "../../common/format";
import { Release } from "../types";
export enum TocType {
Incompatible = "incompatible",
Deduced = "deduced",
Exact = "exact"
}
export interface ReleaseInfo {
id: string;
@@ -6,7 +13,12 @@ export interface ReleaseInfo {
artist: string;
barcode?: string;
catalogs: string[];
tocs: string[];
tocType: TocType;
}
function isFormatTocCompatible(format: string): boolean {
format = format.toLowerCase().replace(/[^a-z+]/g, "");
return format.includes("digitalmedia") || format.includes("cd") || format.includes("disc");
}
export function getPageReleaseId(): string {
@@ -14,31 +26,77 @@ export function getPageReleaseId(): string {
}
export function getPageReleaseInfo(): ReleaseInfo | null {
const releaseId = getPageReleaseId();
let barcode = document.querySelector<HTMLElement>(".barcode")?.innerText;
if (barcode === "[none]") barcode = undefined;
const tocs: string[] = [];
let offset = 0;
try {
for (const mediumEl of document.querySelectorAll("table.medium")) {
tocs.push("0");
for (const durationEl of mediumEl.querySelectorAll("td.treleases")) {
offset += parseDuration(durationEl.innerHTML) * 75;
tocs[tocs.length - 1] += `:${offset}`;
}
}
} catch {}
const format = document.querySelector<HTMLElement>("dd.format")!.innerText;
const tocEl = document.querySelector(".tabs a[href$='/discids']");
const hasExactToc = tocEl && tocEl.textContent !== "Disc IDs (0)";
return {
id: getPageReleaseId(),
id: releaseId,
title: document.querySelector<HTMLElement>("h1 a")!.innerText,
artist: document.querySelector<HTMLElement>(".subheader bdi")!.innerText,
barcode,
catalogs: Array.from(document.querySelectorAll<HTMLElement>(".catalog-number"))
.map(el => el.innerText)
.filter(catalog => catalog != "[none]"),
tocs
tocType: hasExactToc ? TocType.Exact : isFormatTocCompatible(format) ? TocType.Deduced : TocType.Incompatible
};
}
export async function getReleaseToc(releaseInfo: ReleaseInfo): Promise<string | null> {
if (releaseInfo.tocType === TocType.Incompatible) return null;
if (releaseInfo.tocType === TocType.Deduced) {
const toc = getPageReleaseToc();
if (toc) return toc;
}
try {
const release = await fetch(`https://musicbrainz.org/ws/2/release/${releaseInfo.id}?fmt=json&inc=recordings+discids`)
.then((res) => res.json<Release>());
const exactDisc = release.media
.flatMap(media => media.discs)
.filter(disc => disc.offsets.length)[0];
if (exactDisc) return [ 1, exactDisc.offsets.length, exactDisc.sectors ].concat(exactDisc.offsets).join(" ");
const deducedDisc = release.media
.filter(media => isFormatTocCompatible(media.format))[0];
if (deducedDisc) {
let toc = "0";
let offset = 0;
for (const track of deducedDisc.tracks) {
offset += track.length / 1000 * 75;
toc += `:${offset}`;
}
return toc;
}
} catch (err) {
console.error(err);
}
return getPageReleaseToc();
}
function getPageReleaseToc(): string | null {
const tocs: string[] = [];
let offset = 0;
for (const mediumEl of document.querySelectorAll("table.medium")) {
tocs.push("0");
for (const durationEl of mediumEl.querySelectorAll("td.treleases")) {
offset += parseDuration(durationEl.innerHTML) * 75;
tocs[tocs.length - 1] += `:${offset}`;
}
}
return tocs.length ? tocs[0] : null;
}

View File

@@ -1,13 +1,42 @@
/* Better fit buttons on cover edit page. */
span.fileinput-button.buttons {
display: inline-flex;
flex-direction: column;
gap: .5rem;
}
/* Minimize sidebar layout shift on release page */
/* Minimize sidebar layout shift on release page. */
.cover-art-image img {
width: 100%;
aspect-ratio: 1;
object-fit: contain;
}
/* Style dropdown on add cover art page. */
.buttons select {
float: left;
margin: 0 7px 0 0;
background-image: none;
background-color: #EEE;
border: 1px solid #CCC;
border-top: 1px solid #EEE;
border-left: 1px solid #EEE;
font-family: "Lucida Grande",Tahoma,Arial,Verdana,sans-serif;
font-size: 1rem;
line-height: 130%;
text-decoration: none;
font-weight: 700;
color: #666;
cursor: pointer;
padding: 5px 10px 6px 7px;
}
.buttons select:hover,
.buttons select:focus {
background-color: #DFF4FF;
border: 1px solid #C2E1EF;
color: #369;
}

18
src/musicbrainz/types.ts Normal file
View File

@@ -0,0 +1,18 @@
export interface Disc {
offsets: number[];
sectors: number;
}
export interface Track {
length: number;
}
export interface Media {
format: string;
discs: Disc[];
tracks: Track[];
}
export interface Release {
media: Media[];
}