Update Apple Music and VGMdb

This commit is contained in:
SuperSaltyGamer
2023-07-21 13:44:50 +03:00
parent 4170d1f033
commit 138ee4b410
15 changed files with 283 additions and 122 deletions

View File

@@ -12,8 +12,9 @@ Install [ViolentMonkey](https://violentmonkey.github.io) or [TamperMonkey](https
### References
Inspired by and used for reference:
Inspired by and/or used for reference:
* https://github.com/lisonge/vite-plugin-monkey
* https://github.com/chocolateboy/gm-compat
* https://gist.github.com/bunnykek/7f099f55fc558f398cb4cedf6c02c794
* https://github.com/ToadKing/apple-music-barcode-isrc

File diff suppressed because one or more lines are too long

14
dist/vgmdb.user.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +1,7 @@
// ==UserScript==
// @namespace ame-applemusic
// @name Ame (Apple Music)
// @version 1.6.2
// @version 1.7.0
// @author SuperSaltyGamer
// @run-at document-start
// @match https://music.apple.com/*

View File

@@ -3,9 +3,10 @@ import { registerPlugin, AutoColumnSize, ManualColumnMove, CopyPaste, DragToScro
import infoIcon from "../assets/icons/info.svg?raw";
import { offAlbumRoute, onAlbumRoute } from "../glue/routing";
import { createButtonElement } from "../glue/sidebar";
import { getAlbum, getStorefronts } from "../services";
import { getAccountStorefront, getAlbum, getStorefronts } from "../services";
import { fetchCors, fromHTML } from "../../common";
import { Album, Resource } from "../types";
import { ripLyrics } from "./lyrics";
registerPlugin(AutoColumnSize);
registerPlugin(ManualColumnMove);
@@ -97,7 +98,7 @@ async function showDock() {
const albumId = location.pathname.split("/")[4];
let activeAlbum: Resource<Album> | null = null;
let country = localStorage.getItem("ame-info-country") || location.pathname.split("/")[1];
let activeStorefront = localStorage.getItem("ame-info-storefront") || location.pathname.split("/")[1];
const storefronts = await getStorefronts();
dockEl = fromHTML(`
@@ -105,9 +106,10 @@ async function showDock() {
<div id="ame-dock-title">Album Info</div>
<div id="ame-dock-control">
<select id="ame-dock-control-storefront">
${storefronts.map(storefront => `<option value="${storefront.id}" ${storefront.id === country ? "selected" : ""}>${storefront.attributes.name}</option>`).join("")}
${storefronts.map(storefront => `<option value="${storefront.id}" ${storefront.id === activeStorefront ? "selected" : ""}>${storefront.attributes.name}</option>`).join("")}
</select>
<button id="ame-dock-control-isrc2mb">ISRC2MB</button>
<button id="ame-dock-control-lyrics">LYRICS (${getAccountStorefront()?.toUpperCase() || "N/A"})</button>
</div>
<div id="ame-dock-table"></div>
</div>
@@ -116,6 +118,7 @@ async function showDock() {
const titleEl = dockEl.querySelector<HTMLElement>("#ame-dock-title")!;
const storefrontEl = dockEl.querySelector<HTMLSelectElement>("#ame-dock-control-storefront")!;
const isrc2mbEl = dockEl.querySelector<HTMLSelectElement>("#ame-dock-control-isrc2mb")!;
const lyricsEl = dockEl.querySelector<HTMLSelectElement>("#ame-dock-control-lyrics")!;
const tableEl = dockEl.querySelector<HTMLElement>("#ame-dock-table")!;
titleEl.addEventListener("click", () => {
@@ -136,6 +139,20 @@ async function showDock() {
open(`https://magicisrc.kepstin.ca/?${params.toString()}`, "_blank");
});
let isRippingLyrics = false;
lyricsEl.addEventListener("click", async () => {
if (isRippingLyrics) return;
isRippingLyrics = true;
try {
await ripLyrics(albumId);
} catch (err) {
console.error(err);
}
isRippingLyrics = false;
});
let columns: (keyof Row)[] = JSON.parse(localStorage.getItem("ame-info-columns") || "[]");
if (columns.length !== COLUMN_ORDER.length) columns = COLUMN_ORDER;
@@ -177,7 +194,7 @@ async function showDock() {
containerEl.style.paddingBottom = dockEl!.clientHeight + "px";
async function render() {
const album = await getAlbum(albumId, country);
const album = await getAlbum(albumId, activeStorefront);
if (!album) return;
activeAlbum = album;
@@ -215,8 +232,8 @@ async function showDock() {
await render();
storefrontEl.addEventListener("change", async e => {
country = (e.target as HTMLSelectElement).value;
localStorage.setItem("ame-info-country", country);
activeStorefront = (e.target as HTMLSelectElement).value;
localStorage.setItem("ame-info-storefront", activeStorefront);
await render();
});
}

View File

@@ -0,0 +1,79 @@
import JSZip from "jszip";
import xmlFormatter from "xml-formatter";
import { downloadFile, sleep } from "../../common";
import { getAlbum } from "../services/album";
import { getLyrics } from "../services/lyrics";
import { Lyrics, Track } from "../types";
import { formatPath } from "../../common/format";
import { getAccountStorefront } from "../services";
export async function ripLyrics(albumId: string) {
const accountStorefront = getAccountStorefront();
if (!accountStorefront) {
alert("Lyrics can only be ripped when you have an active subscription in the specific storefront.");
return;
}
const album = await getAlbum(albumId, accountStorefront);
if (!album) return;
const zip = new JSZip();
for (const track of album.relationships.tracks.data) {
if (track.attributes.hasTimeSyncedLyrics) {
const syllableLyrics = await getLyrics(track.id, true, accountStorefront);
if (syllableLyrics) zipLyrics(zip, track.attributes, syllableLyrics.attributes, true);
}
if (track.attributes.hasLyrics) {
const lyrics = await getLyrics(track.id, false, accountStorefront);
if (lyrics) zipLyrics(zip, track.attributes, lyrics.attributes, false);
}
await sleep(100);
}
if (Object.keys(zip.files).length === 0) {
alert("No lyrics found.");
return;
}
const lyricsZip = await zip.generateAsync({ type: "blob" });
downloadFile(lyricsZip, formatPath(`Lyrics {${album.attributes.upc || album.id}}.zip`));
}
function zipLyrics(zip: JSZip, track: Track, lyrics: Lyrics, syllable: boolean) {
const filename = formatPath(`${track.discNumber}-${track.trackNumber.toString().padStart(2, "0")}. ${track.name.slice(0, 120)}`);
// Save raw lyrics.
if (syllable && lyrics.ttml.includes("<span begin=")) {
zip.file(`${filename}.syllable.ttml`, xmlFormatter(lyrics.ttml, { lineSeparator: "\n", indentation: "\t" }));
} else {
zip.file(`${filename}.ttml`, xmlFormatter(lyrics.ttml, { lineSeparator: "\n", indentation: "\t" }));
}
// Save converted lyrics.
if (!syllable) {
const lyricsDoc = new DOMParser().parseFromString(lyrics.ttml, "text/xml");
let out = "";
for (const lineNode of Array.from(lyricsDoc.querySelectorAll("p")) as HTMLParagraphElement[]) {
const timestamp = lineNode.getAttribute("begin");
if (timestamp) {
out += `[${convertTimestamp(timestamp)}] ${lineNode.textContent}\n`;
} else {
out += `${lineNode.textContent}\n`;
}
}
zip.file(`${filename}.lrc`, out + "\n");
}
}
function convertTimestamp(timestamp: string): string {
const parts = timestamp.split(/[:.]/g).reverse();
const mm = (parts[2] ?? "").padStart(2, "0");
const ss = (parts[1] ?? "").padStart(2, "0");
const xx = Math.floor(Number(parts[0]) / 10).toString().padStart(2, "0");
return `${mm}:${ss}.${xx}`;
}

View File

@@ -2,10 +2,10 @@ import { fetchCors } from '../../common';
import { Album, ApiResponse, Resource } from '../types';
import { getAuthToken } from './auth';
export async function getAlbum(id: string, country?: string): Promise<Resource<Album> | null> {
country ??= location.pathname.split('/')[1];
export async function getAlbum(id: string, storefront?: string): Promise<Resource<Album> | null> {
storefront ??= location.pathname.split('/')[1];
const res = await fetchCors(`https://amp-api.music.apple.com/v1/catalog/${country}/albums/${id}?extend=extendedAssetUrls`, {
const res = await fetchCors(`https://amp-api.music.apple.com/v1/catalog/${storefront}/albums/${id}?extend=extendedAssetUrls`, {
headers: {
'Origin': 'https://music.apple.com',
'Referer': 'https://music.apple.com/',

View File

@@ -1,4 +1,4 @@
import { fetchCors } from '../../common';
import { fetchCors, readCookies } from '../../common';
let cachedAuthToken = '';
@@ -17,3 +17,13 @@ export async function getAuthToken(): Promise<string> {
cachedAuthToken = match[0];
return cachedAuthToken;
}
export function getUserToken(): string | null {
const cookies = readCookies();
return cookies["music-user-token"] || null;
}
export function getAccountStorefront(): string | null {
const cookies = readCookies();
return cookies["itua"] || null;
}

View File

@@ -0,0 +1,20 @@
import { fetchCors } from "../../common";
import { ApiResponse, Lyrics, Resource } from "../types";
import { getAuthToken } from "./auth";
export async function getLyrics(id: string, syllable: boolean, storefront?: string): Promise<Resource<Lyrics> | null> {
storefront ??= location.pathname.split("/")[1];
const res = await fetchCors(`https://amp-api.music.apple.com/v1/catalog/${storefront}/songs/${id}/${syllable ? "syllable-lyrics" : "lyrics"}`, {
headers: {
"Origin": "https://music.apple.com",
"Referer": "https://music.apple.com/",
"Authorization": `Bearer ${await getAuthToken()}`
}
});
if (res.status === 404) return null;
const lyrics = await res.json<ApiResponse<Lyrics>>();
return lyrics.data[0];
}

View File

@@ -233,8 +233,11 @@ dialog *,
}
#ame-dock-control {
display: flex;
padding: .5rem;
background-color: var(--pageBG);
align-items: center;
gap: 1rem;
}
#ame-dock-control input,
@@ -243,5 +246,6 @@ dialog *,
}
#ame-dock-control button {
padding: .1rem .25rem;
margin-top: 1px;
padding: .1rem;
}

View File

@@ -58,3 +58,7 @@ export interface Track {
plus: string;
};
}
export interface Lyrics {
ttml: string;
}

View File

@@ -1,6 +1,10 @@
export * from './fetch';
export * from './router';
export function readCookies(): Record<string, string> {
return Object.fromEntries(document.cookie.split("; ").map(cookie => cookie.split("=", 2)));
}
export function sleep(delay: number): Promise<void> {
return new Promise<void>((resolve) => {
setTimeout(resolve, delay);

View File

@@ -1,7 +1,7 @@
// ==UserScript==
// @namespace ame-vgmdb
// @name Ame (VGMdb)
// @version 1.1.1
// @version 1.1.2
// @author SuperSaltyGamer
// @run-at document-end
// @match https://vgmdb.net/*

View File

@@ -36,15 +36,15 @@ async function downloadScans() {
const zip = new JSZip();
const albumInfo = getAlbumInfo()!;
const foldername = formatPath(`${albumInfo.catalog ?? albumInfo.barcode ?? "Scans"}`);
const foldername = formatPath(`Scans {${albumInfo.catalog || albumInfo.barcode || albumInfo.id}}`);
const els = Array.from(document.querySelectorAll<HTMLLinkElement>(`#cover_gallery a[href^="https://media.vgm.io"]`));
for (const el of els) {
const data = await fetchCors(el.href).then(res => res.blob());
const filename = formatPath(el.querySelector('h4')!.innerText.trim());
zip.file(`${foldername}/${filename}.jpg`, data);
zip.file(`${filename}.jpg`, data);
await sleep(100);
}
downloadFile(await zip.generateAsync({ type: "blob" }), `${formatPath(foldername)}.zip`);
downloadFile(await zip.generateAsync({ type: "blob" }), `${foldername}.zip`);
}

View File

@@ -1,4 +1,5 @@
export interface AlbumInfo {
id: string;
artist: string;
album: string;
label?: string;
@@ -29,6 +30,7 @@ export function getAlbumInfo(): AlbumInfo {
: [ artistTitle, "" ];
const info: AlbumInfo = {
id: location.pathname.split("/")[2],
artist,
album: title,
mediums: [],