mirror of
https://notabug.org/SuperSaltyGamer/ame
synced 2026-01-15 19:32:54 -03:00
[musicbrainz] Add advanced lookup support for covers
This commit is contained in:
28
dist/musicbrainz.user.js
vendored
28
dist/musicbrainz.user.js
vendored
File diff suppressed because one or more lines are too long
@@ -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/*
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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])}`);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { getPageReleaseId } from "../services/release";
|
||||
|
||||
enum QueryType {
|
||||
Unknown = "unknown",
|
||||
Catalog = "catalog",
|
||||
Barcode = "barcode",
|
||||
Catalog = "catalog",
|
||||
Isrc = "isrc",
|
||||
Toc = "toc"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
18
src/musicbrainz/types.ts
Normal 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[];
|
||||
}
|
||||
Reference in New Issue
Block a user