mirror of
https://notabug.org/SuperSaltyGamer/ame
synced 2026-01-15 19:22:55 -03:00
Update Apple Music and VGMdb
This commit is contained in:
@@ -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
|
||||
|
||||
216
dist/applemusic.user.js
vendored
216
dist/applemusic.user.js
vendored
File diff suppressed because one or more lines are too long
14
dist/vgmdb.user.js
vendored
14
dist/vgmdb.user.js
vendored
File diff suppressed because one or more lines are too long
@@ -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/*
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
|
||||
79
src/applemusic/modules/lyrics.ts
Normal file
79
src/applemusic/modules/lyrics.ts
Normal 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}`;
|
||||
}
|
||||
@@ -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/',
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
20
src/applemusic/services/lyrics.ts
Normal file
20
src/applemusic/services/lyrics.ts
Normal 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];
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -58,3 +58,7 @@ export interface Track {
|
||||
plus: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Lyrics {
|
||||
ttml: string;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -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`);
|
||||
}
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
Reference in New Issue
Block a user