Compare commits
3 Commits
3ee8361669
...
20a91ce2aa
Author | SHA1 | Date | |
---|---|---|---|
20a91ce2aa | |||
93e13f9668 | |||
57fd2f626b |
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,4 +9,5 @@ node_modules
|
|||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
data/pending
|
data/pending
|
||||||
.vscode
|
.vscode
|
||||||
|
.idea
|
11
package.json
11
package.json
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "aquavox",
|
"name": "aquavox",
|
||||||
"version": "2.3.2",
|
"version": "2.4.0",
|
||||||
"private": false,
|
"private": false,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@ -11,7 +11,7 @@
|
|||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"lint": "prettier --check . && eslint .",
|
"lint": "prettier --check . && eslint .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"go": "PORT=4173 bun ./build"
|
"go": "PORT=4173 deno ./build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/svelte": "^4.0.2",
|
"@iconify/svelte": "^4.0.2",
|
||||||
@ -29,11 +29,11 @@
|
|||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-svelte": "^3.2.3",
|
"prettier-plugin-svelte": "^3.2.3",
|
||||||
"svelte": "^4.2.17",
|
"svelte": "^4.2.19",
|
||||||
"svelte-check": "^3.7.1",
|
"svelte-check": "^3.7.1",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.11",
|
"vite": "5.4.6",
|
||||||
"vite-plugin-wasm": "^3.3.0",
|
"vite-plugin-wasm": "^3.3.0",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
@ -61,6 +61,7 @@
|
|||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
"rollup-plugin-node-polyfills": "^0.2.1",
|
"rollup-plugin-node-polyfills": "^0.2.1",
|
||||||
"typescript-parsec": "^0.3.4",
|
"typescript-parsec": "^0.3.4",
|
||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1",
|
||||||
|
"vite-tsconfig-paths": "^5.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
15
src/lib/components/homepage/headBar.svelte
Normal file
15
src/lib/components/homepage/headBar.svelte
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<div class="fixed bg-white dark:bg-[#1f1f1f] !bg-opacity-70 h-16 w-full z-50 backdrop-blur-xl flex justify-center items-center">
|
||||||
|
<div class="lg:w-72 xl:w-80">
|
||||||
|
<div class="flex items-center justify-between routers">
|
||||||
|
<a href="/home">主页</a>
|
||||||
|
<a href="/discover">探索</a>
|
||||||
|
<a href="/library">音乐库</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
.routers a {
|
||||||
|
@apply hover:bg-neutral-300 hover:dark:bg-neutral-700 hover:!bg-opacity-40 rounded-md py-2 px-6 text-lg duration-150;
|
||||||
|
}
|
||||||
|
</style>
|
@ -5,7 +5,6 @@
|
|||||||
import type { Spring } from '$lib/graphics/spring/spring';
|
import type { Spring } from '$lib/graphics/spring/spring';
|
||||||
|
|
||||||
const viewportWidth = document.documentElement.clientWidth;
|
const viewportWidth = document.documentElement.clientWidth;
|
||||||
const scaleCurrentLine = viewportWidth > 640 ? 1.02 : 1.045 ;
|
|
||||||
|
|
||||||
export let line: ScriptItem;
|
export let line: ScriptItem;
|
||||||
export let index: number;
|
export let index: number;
|
||||||
@ -18,7 +17,6 @@
|
|||||||
let time = 0;
|
let time = 0;
|
||||||
let positionX: number = 0;
|
let positionX: number = 0;
|
||||||
let positionY: number = 0;
|
let positionY: number = 0;
|
||||||
let scale = 1;
|
|
||||||
let opacity = 1;
|
let opacity = 1;
|
||||||
let stopped = false;
|
let stopped = false;
|
||||||
let lastPosX: number | undefined = undefined;
|
let lastPosX: number | undefined = undefined;
|
||||||
@ -76,7 +74,6 @@
|
|||||||
export const setCurrent = (isCurrent: boolean) => {
|
export const setCurrent = (isCurrent: boolean) => {
|
||||||
isCurrentLyric = isCurrent;
|
isCurrentLyric = isCurrent;
|
||||||
opacity = isCurrent ? 1 : 0.36;
|
opacity = isCurrent ? 1 : 0.36;
|
||||||
scale = isCurrent ? scaleCurrentLine : 1;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setBlur = (blur: number) => {
|
export const setBlur = (blur: number) => {
|
||||||
@ -112,8 +109,8 @@
|
|||||||
lastPosY = pos.y;
|
lastPosY = pos.y;
|
||||||
positionX = pos.x;
|
positionX = pos.x;
|
||||||
positionY = pos.y;
|
positionY = pos.y;
|
||||||
springX = createSpring(pos.x, pos.x, 0.126, 0.8);
|
springX = createSpring(pos.x, pos.x, 0.114, 0.72);
|
||||||
springY = createSpring(pos.y, pos.y, 0.126, 0.8);
|
springY = createSpring(pos.y, pos.y, 0.114, 0.72);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stop = () => {
|
export const stop = () => {
|
||||||
@ -126,31 +123,36 @@
|
|||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div
|
<div
|
||||||
style="transform: translate3d({positionX}px, {positionY}px, 0); scale: {scale};
|
style="transform: translate3d({positionX}px, {positionY}px, 0); transition-property: opacity, text-shadow;
|
||||||
transition-property: scale, opacity; transition-duration: 0.5s; transition-timing-function: ease-in-out; opacity: {opacity};
|
transition-duration: 0.36s; transition-timing-function: ease-out; opacity: {opacity};
|
||||||
transform-origin: center left;"
|
transform-origin: center left;"
|
||||||
class="absolute z-50 w-full pr-12 lg:pr-16 cursor-default py-5"
|
class="absolute z-50 w-full pr-12 lg:pr-16 cursor-default py-5"
|
||||||
bind:this={ref}
|
bind:this={ref}
|
||||||
on:touchstart={() => {
|
on:touchstart={() => {
|
||||||
clickMask.style.backgroundColor = "rgba(255,255,255,.3)";
|
clickMask.style.backgroundColor = 'rgba(255,255,255,.3)';
|
||||||
}}
|
}}
|
||||||
on:touchend={() => {
|
on:touchend={() => {
|
||||||
clickMask.style.backgroundColor = "transparent";
|
clickMask.style.backgroundColor = 'transparent';
|
||||||
}}
|
}}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
lyricClick(index);
|
lyricClick(index);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span class="absolute w-[calc(100%-2.5rem)] lg:w-[calc(100%-2.75rem)] h-full
|
<span
|
||||||
-translate-x-2 lg:-translate-x-5 -translate-y-5 rounded-lg duration-300 lg:hover:bg-[rgba(255,255,255,.15)]" bind:this={clickMask}>
|
class="absolute w-[calc(100%-2.5rem)] lg:w-[calc(100%-3rem)] h-full
|
||||||
|
-translate-x-2 lg:-translate-x-5 -translate-y-5 rounded-lg duration-300 lg:hover:bg-[rgba(255,255,255,.15)]"
|
||||||
|
bind:this={clickMask}
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
{#if debugMode}
|
{#if debugMode}
|
||||||
<span class="text-lg absolute -translate-y-7">
|
<span class="text-lg absolute -translate-y-7">
|
||||||
{index}: duration: {(line.end - line.start).toFixed(3)}, {line.start.toFixed(3)}~{line.end.toFixed(3)}
|
{index}: duration: {(line.end - line.start).toFixed(3)}, {line.start.toFixed(3)}~{line.end.toFixed(3)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
<span class={`text-white text-[2rem] leading-9 lg:text-5xl lg:leading-[4rem] font-semibold text-shadow-lg mr-4`}>
|
<span
|
||||||
|
class={`text-white text-[2rem] leading-9 lg:text-5xl lg:leading-[4rem] font-semibold text-shadow-lg mr-4
|
||||||
|
${isCurrentLyric ? 'text-glow' : ''}`}
|
||||||
|
>
|
||||||
{line.text}
|
{line.text}
|
||||||
</span>
|
</span>
|
||||||
{#if line.translation}
|
{#if line.translation}
|
||||||
@ -160,3 +162,13 @@
|
|||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.text-glow {
|
||||||
|
text-shadow:
|
||||||
|
0 0 3px #ffffff2c,
|
||||||
|
0 0 6px #ffffff2c,
|
||||||
|
0 15px 30px rgba(0, 0, 0, 0.11),
|
||||||
|
0 5px 15px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { LrcJsonData } from '$lib/lyrics/type';
|
import type { LrcJsonData, ScriptItem } from '$lib/lyrics/type';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import type { ScriptItem } from '$lib/lyrics/type';
|
|
||||||
import LyricLine from './lyricLine.svelte';
|
import LyricLine from './lyricLine.svelte';
|
||||||
import createLyricsSearcher from '$lib/lyrics/lyricSearcher';
|
import createLyricsSearcher from '$lib/lyrics/lyricSearcher';
|
||||||
|
|
||||||
// constants
|
// constants
|
||||||
const viewportHeight = document.documentElement.clientHeight;
|
const viewportHeight = document.documentElement.clientHeight;
|
||||||
const viewportWidth = document.documentElement.clientWidth;
|
const viewportWidth = document.documentElement.clientWidth;
|
||||||
const marginY = viewportWidth > 640 ? 36 : 0 ;
|
const marginY = viewportWidth > 640 ? 12 : 0;
|
||||||
const blurRatio = viewportWidth > 640 ? 1 : 1.4;
|
const blurRatio = viewportWidth > 640 ? 1 : 1.4;
|
||||||
const currentLyrictTop = viewportWidth > 640 ? viewportHeight * 0.12 : viewportHeight * 0.05;
|
const currentLyricTop = viewportWidth > 640 ? viewportHeight * 0.12 : viewportHeight * 0.05;
|
||||||
const deceleration = 0.95; // Velocity decay factor for inertia
|
const deceleration = 0.95; // Velocity decay factor for inertia
|
||||||
const minVelocity = 0.1; // Minimum velocity to stop inertia
|
const minVelocity = 0.1; // Minimum velocity to stop inertia
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
@ -57,7 +56,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initLyricTopList() {
|
function initLyricTopList() {
|
||||||
let cumulativeHeight = 0;
|
let cumulativeHeight = currentLyricTop;
|
||||||
for (let i = 0; i < lyricLines.length; i++) {
|
for (let i = 0; i < lyricLines.length; i++) {
|
||||||
const c = lyricComponents[i];
|
const c = lyricComponents[i];
|
||||||
lyricElements.push(c.getRef());
|
lyricElements.push(c.getRef());
|
||||||
@ -73,14 +72,16 @@
|
|||||||
if (!originalLyrics.scripts) return;
|
if (!originalLyrics.scripts) return;
|
||||||
const currentLyricDuration =
|
const currentLyricDuration =
|
||||||
originalLyrics.scripts[currentLyricIndex].end - originalLyrics.scripts[currentLyricIndex].start;
|
originalLyrics.scripts[currentLyricIndex].end - originalLyrics.scripts[currentLyricIndex].start;
|
||||||
const relativeOrigin = lyricTopList[currentLyricIndex] - currentLyrictTop;
|
const relativeOrigin = lyricTopList[currentLyricIndex] - currentLyricTop;
|
||||||
for (let i = 0; i < lyricElements.length; i++) {
|
for (let i = 0; i < lyricElements.length; i++) {
|
||||||
const currentLyricComponent = lyricComponents[i];
|
const currentLyricComponent = lyricComponents[i];
|
||||||
let delay = 0;
|
let delay = 0;
|
||||||
if (i <= currentLyricIndex) {
|
if (i < currentLyricIndex) {
|
||||||
delay = 0;
|
delay = 0;
|
||||||
|
} else if (i == currentLyricIndex) {
|
||||||
|
delay = 0.042;
|
||||||
} else {
|
} else {
|
||||||
delay = 0.013 + Math.min(Math.min(currentLyricDuration, 0.1), 0.075 * (i - currentLyricIndex));
|
delay = Math.min(Math.min(currentLyricDuration, 0.6), 0.067 * (i - currentLyricIndex + 1.2));
|
||||||
}
|
}
|
||||||
const offset = Math.abs(i - currentLyricIndex);
|
const offset = Math.abs(i - currentLyricIndex);
|
||||||
let blurRadius = Math.min(offset * blurRatio, 16);
|
let blurRadius = Math.min(offset * blurRatio, 16);
|
||||||
@ -151,6 +152,7 @@
|
|||||||
velocityY *= deceleration; // Apply deceleration to velocity
|
velocityY *= deceleration; // Apply deceleration to velocity
|
||||||
inertiaFrame = requestAnimationFrame(inertiaScroll); // Continue scrolling in next frame
|
inertiaFrame = requestAnimationFrame(inertiaScroll); // Continue scrolling in next frame
|
||||||
}
|
}
|
||||||
|
|
||||||
inertiaScroll();
|
inertiaScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -179,7 +181,6 @@
|
|||||||
$: {
|
$: {
|
||||||
if (lyricsContainer && lyricComponents.length > 0) {
|
if (lyricsContainer && lyricComponents.length > 0) {
|
||||||
if (progress >= nextUpdate - 0.5 && !scrolling) {
|
if (progress >= nextUpdate - 0.5 && !scrolling) {
|
||||||
console.log("computeLayout")
|
|
||||||
computeLayout();
|
computeLayout();
|
||||||
}
|
}
|
||||||
if (Math.abs(lastProgress - progress) > 0.5) {
|
if (Math.abs(lastProgress - progress) > 0.5) {
|
||||||
@ -194,8 +195,7 @@
|
|||||||
const nextStart = originalLyrics.scripts![Math.min(currentLyricIndex + 1, lyricLength - 1)].start;
|
const nextStart = originalLyrics.scripts![Math.min(currentLyricIndex + 1, lyricLength - 1)].start;
|
||||||
if (currentEnd !== nextStart) {
|
if (currentEnd !== nextStart) {
|
||||||
nextUpdate = currentEnd;
|
nextUpdate = currentEnd;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
nextUpdate = nextStart;
|
nextUpdate = nextStart;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,7 +205,15 @@
|
|||||||
|
|
||||||
$: {
|
$: {
|
||||||
for (let i = 0; i < lyricElements.length; i++) {
|
for (let i = 0; i < lyricElements.length; i++) {
|
||||||
const isCurrent = i == currentLyricIndex;
|
const s = originalLyrics.scripts![i].start;
|
||||||
|
const t = originalLyrics.scripts![i].end;
|
||||||
|
// Explain:
|
||||||
|
// The `currentLyricIndex` is also used for locating & layout computing,
|
||||||
|
// so when the current progress is in the interlude between two lyrics,
|
||||||
|
// `currentLyricIndex` still needs to have a valid value to ensure that
|
||||||
|
// the style and scrolling position are calculated correctly.
|
||||||
|
// But in that situation, the “current lyric index” does not exist.
|
||||||
|
const isCurrent = i == currentLyricIndex && s <= progress && progress <= t;
|
||||||
const currentLyricComponent = lyricComponents[i];
|
const currentLyricComponent = lyricComponents[i];
|
||||||
currentLyricComponent.setCurrent(isCurrent);
|
currentLyricComponent.setCurrent(isCurrent);
|
||||||
}
|
}
|
||||||
|
@ -1,270 +0,0 @@
|
|||||||
import {
|
|
||||||
alt_sc,
|
|
||||||
apply,
|
|
||||||
buildLexer,
|
|
||||||
expectEOF,
|
|
||||||
fail,
|
|
||||||
kleft,
|
|
||||||
kmid,
|
|
||||||
kright,
|
|
||||||
opt_sc,
|
|
||||||
type Parser,
|
|
||||||
rep,
|
|
||||||
rep_sc,
|
|
||||||
seq,
|
|
||||||
str,
|
|
||||||
tok,
|
|
||||||
type Token
|
|
||||||
} from 'typescript-parsec';
|
|
||||||
import type { LrcJsonData, ParsedLrc, ScriptItem, ScriptWordsItem } from '../type';
|
|
||||||
import type { IDTag } from './type';
|
|
||||||
|
|
||||||
|
|
||||||
interface ParserScriptItem {
|
|
||||||
start: number;
|
|
||||||
text: string;
|
|
||||||
words?: ScriptWordsItem[];
|
|
||||||
translation?: string;
|
|
||||||
singer?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertTimeToMs({
|
|
||||||
mins,
|
|
||||||
secs,
|
|
||||||
decimals
|
|
||||||
}: {
|
|
||||||
mins?: number | string;
|
|
||||||
secs?: number | string;
|
|
||||||
decimals?: string;
|
|
||||||
}) {
|
|
||||||
let result = 0;
|
|
||||||
if (mins) {
|
|
||||||
result += Number(mins) * 60 * 1000;
|
|
||||||
}
|
|
||||||
if (secs) {
|
|
||||||
result += Number(secs) * 1000;
|
|
||||||
}
|
|
||||||
if (decimals) {
|
|
||||||
const denom = Math.pow(10, decimals.length);
|
|
||||||
result += Number(decimals) / (denom / 1000);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
const digit = Array.from({ length: 10 }, (_, i) => apply(str(i.toString()), (_) => i)).reduce(
|
|
||||||
(acc, cur) => alt_sc(cur, acc),
|
|
||||||
fail('no alternatives')
|
|
||||||
);
|
|
||||||
const numStr = apply(rep_sc(digit), (r) => r.join(''));
|
|
||||||
const num = apply(numStr, (r) => parseInt(r));
|
|
||||||
const alpha = alt_sc(
|
|
||||||
Array.from({ length: 26 }, (_, i) =>
|
|
||||||
apply(str(String.fromCharCode('a'.charCodeAt(0) + i)), (_) => String.fromCharCode('a'.charCodeAt(0) + i))
|
|
||||||
).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives')),
|
|
||||||
Array.from({ length: 26 }, (_, i) =>
|
|
||||||
apply(str(String.fromCharCode('A'.charCodeAt(0) + i)), (_) => String.fromCharCode('A'.charCodeAt(0) + i))
|
|
||||||
).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives'))
|
|
||||||
);
|
|
||||||
|
|
||||||
const alphaStr = apply(rep(alpha), (r) => r.join(''));
|
|
||||||
|
|
||||||
function spaces<K>(): Parser<K, Token<K>[]> {
|
|
||||||
return rep_sc(str(' '));
|
|
||||||
}
|
|
||||||
|
|
||||||
const unicodeStr = rep(tok('char'));
|
|
||||||
|
|
||||||
function trimmed<K, T>(p: Parser<K, Token<T>[]>): Parser<K, Token<T>[]> {
|
|
||||||
return apply(p, (r) => {
|
|
||||||
while (r.length > 0 && r[0].text.trim() === '') {
|
|
||||||
r.shift();
|
|
||||||
}
|
|
||||||
while (r.length > 0 && r[r.length - 1].text.trim() === '') {
|
|
||||||
r.pop();
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function padded<K, T>(p: Parser<K, T>): Parser<K, T> {
|
|
||||||
return kmid(spaces(), p, spaces());
|
|
||||||
}
|
|
||||||
|
|
||||||
function anythingTyped(types: string[]) {
|
|
||||||
return types.map((t) => tok(t)).reduce((acc, cur) => alt_sc(cur, acc), fail('no alternatives'));
|
|
||||||
}
|
|
||||||
|
|
||||||
function lrcTimestamp<K, T>(delim: [Parser<K, Token<T>>, Parser<K, Token<T>>]) {
|
|
||||||
const innerTS = alt_sc(
|
|
||||||
apply(seq(num, str(':'), num, str('.'), numStr), (r) =>
|
|
||||||
convertTimeToMs({ mins: r[0], secs: r[2], decimals: r[4] })
|
|
||||||
),
|
|
||||||
apply(seq(num, str('.'), numStr), (r) => convertTimeToMs({ secs: r[0], decimals: r[2] })),
|
|
||||||
apply(seq(num, str(':'), num), (r) => convertTimeToMs({ mins: r[0], secs: r[2] })),
|
|
||||||
apply(num, (r) => convertTimeToMs({ secs: r }))
|
|
||||||
);
|
|
||||||
return kmid(delim[0], innerTS, delim[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
const squareTS = lrcTimestamp([tok('['), tok(']')]);
|
|
||||||
const angleTS = lrcTimestamp([tok('<'), tok('>')]);
|
|
||||||
|
|
||||||
const lrcTag = apply(
|
|
||||||
seq(
|
|
||||||
tok('['),
|
|
||||||
alphaStr,
|
|
||||||
str(':'),
|
|
||||||
tokenParserToText(trimmed(rep(anythingTyped(['char', '[', ']', '<', '>'])))),
|
|
||||||
tok(']')
|
|
||||||
),
|
|
||||||
(r) => ({
|
|
||||||
[r[1]]: r[3]
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
function joinTokens<T>(tokens: Token<T>[]) {
|
|
||||||
return tokens.map((t) => t.text).join('');
|
|
||||||
}
|
|
||||||
|
|
||||||
function tokenParserToText<K, T>(p: Parser<K, Token<T>> | Parser<K, Token<T>[]>): Parser<K, string> {
|
|
||||||
return apply(p, (r: Token<T> | Token<T>[]) => {
|
|
||||||
if (Array.isArray(r)) {
|
|
||||||
return joinTokens(r);
|
|
||||||
}
|
|
||||||
return r.text;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const singerIndicator = kleft(tok('char'), str(':'));
|
|
||||||
const translateParser = kright(tok('|'), unicodeStr);
|
|
||||||
|
|
||||||
function lrcLine(
|
|
||||||
wordDiv = ' ', legacy = false
|
|
||||||
): Parser<unknown, ['script_item', ParserScriptItem] | ['lrc_tag', IDTag] | ['comment', string] | ['empty', null]> {
|
|
||||||
return alt_sc(
|
|
||||||
legacy ? apply(seq(squareTS, trimmed(rep_sc(anythingTyped(['char', '[', ']', '<', '>'])))), (r) =>
|
|
||||||
['script_item', { start: r[0], text: joinTokens(r[1]) } as ParserScriptItem] // TODO: Complete this
|
|
||||||
) : apply(
|
|
||||||
seq(
|
|
||||||
squareTS,
|
|
||||||
opt_sc(padded(singerIndicator)),
|
|
||||||
rep_sc(
|
|
||||||
seq(
|
|
||||||
opt_sc(angleTS),
|
|
||||||
trimmed(rep_sc(anythingTyped(['char', '[', ']'])))
|
|
||||||
)
|
|
||||||
),
|
|
||||||
opt_sc(trimmed(translateParser))
|
|
||||||
), (r) => {
|
|
||||||
const start = r[0];
|
|
||||||
const singerPart = r[1];
|
|
||||||
const mainPart = r[2];
|
|
||||||
const translatePart = r[3];
|
|
||||||
|
|
||||||
const text = mainPart
|
|
||||||
.map((s) => joinTokens(s[1]))
|
|
||||||
.filter((s) => s.trim().length > 0)
|
|
||||||
.join(wordDiv);
|
|
||||||
|
|
||||||
const words = mainPart
|
|
||||||
.filter((s) => joinTokens(s[1]).trim().length > 0)
|
|
||||||
.map((s) => {
|
|
||||||
const wordBegin = s[0];
|
|
||||||
const word = s[1];
|
|
||||||
let ret: Partial<ScriptWordsItem> = { start: wordBegin };
|
|
||||||
if (word[0]) {
|
|
||||||
ret.beginIndex = word[0].pos.columnBegin - 1;
|
|
||||||
}
|
|
||||||
if (word[word.length - 1]) {
|
|
||||||
ret.endIndex = word[word.length - 1].pos.columnEnd;
|
|
||||||
}
|
|
||||||
return ret as ScriptWordsItem; // TODO: Complete this
|
|
||||||
});
|
|
||||||
|
|
||||||
const singer = singerPart?.text;
|
|
||||||
const translation = translatePart === undefined ? undefined : joinTokens(translatePart);
|
|
||||||
|
|
||||||
return ['script_item', { start, text, words, singer, translation } as ParserScriptItem];
|
|
||||||
}),
|
|
||||||
apply(lrcTag, (r) => ['lrc_tag', r as IDTag]),
|
|
||||||
apply(seq(spaces(), str('#'), unicodeStr), (cmt) => ['comment', cmt[2].join('')] as const),
|
|
||||||
apply(spaces(), (_) => ['empty', null] as const)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dumpToken<T>(t: Token<T> | undefined): string {
|
|
||||||
if (t === undefined) {
|
|
||||||
return '<EOF>';
|
|
||||||
}
|
|
||||||
return '`' + t.text + '` -> ' + dumpToken(t.next);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseLRC(
|
|
||||||
input: string,
|
|
||||||
{ wordDiv, strict, legacy }: { wordDiv?: string; strict?: boolean; legacy?: boolean } = {}
|
|
||||||
): ParsedLrc {
|
|
||||||
const tokenizer = buildLexer([
|
|
||||||
[true, /^\[/gu, '['],
|
|
||||||
[true, /^\]/gu, ']'],
|
|
||||||
[true, /^</gu, '<'],
|
|
||||||
[true, /^>/gu, '>'],
|
|
||||||
[true, /^\|/gu, '|'],
|
|
||||||
[true, /^./gu, 'char']
|
|
||||||
]);
|
|
||||||
|
|
||||||
const lines = input
|
|
||||||
.split(/\r\n|\r|\n/gu)
|
|
||||||
.filter((line) => line.trim().length > 0)
|
|
||||||
.map((line) => tokenizer.parse(line));
|
|
||||||
|
|
||||||
return lines
|
|
||||||
.map((line) => {
|
|
||||||
const res = expectEOF(lrcLine(wordDiv, legacy).parse(line));
|
|
||||||
if (!res.successful) {
|
|
||||||
if (strict) {
|
|
||||||
throw new Error('Failed to parse full line: ' + dumpToken(line));
|
|
||||||
} else {
|
|
||||||
console.error('Failed to parse full line: ' + dumpToken(line));
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return res.candidates[0].result;
|
|
||||||
})
|
|
||||||
.filter((r) => r !== null)
|
|
||||||
.reduce((acc, cur) => {
|
|
||||||
switch (cur[0]) {
|
|
||||||
case 'lrc_tag':
|
|
||||||
Object.assign(acc, cur[1]);
|
|
||||||
return acc;
|
|
||||||
case 'script_item':
|
|
||||||
acc.scripts = acc.scripts || [];
|
|
||||||
acc.scripts.push(cur[1]);
|
|
||||||
return acc;
|
|
||||||
default:
|
|
||||||
return acc;
|
|
||||||
}
|
|
||||||
}, {} as ParsedLrc);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function lrcParser(lrc: string): LrcJsonData {
|
|
||||||
const parsedLrc = parseLRC(lrc, { wordDiv: '', strict: false });
|
|
||||||
if (parsedLrc.scripts === undefined) {
|
|
||||||
return parsedLrc as LrcJsonData;
|
|
||||||
}
|
|
||||||
let finalLrc: LrcJsonData = parsedLrc as LrcJsonData;
|
|
||||||
let lyrics: ScriptItem[] = [];
|
|
||||||
let i = 0;
|
|
||||||
while (i < parsedLrc.scripts.length - 1) {
|
|
||||||
let lyricLine = parsedLrc.scripts[i] as ScriptItem;
|
|
||||||
lyricLine.start/=1000;
|
|
||||||
lyricLine.end = parsedLrc.scripts[i+1].start / 1000;
|
|
||||||
if (parsedLrc.scripts[i+1].text.trim() === "") {
|
|
||||||
i+=2;
|
|
||||||
} else i++;
|
|
||||||
if (lyricLine.text.trim() !== "") {
|
|
||||||
lyrics.push(lyricLine);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finalLrc.scripts = lyrics;
|
|
||||||
return finalLrc;
|
|
||||||
}
|
|
@ -1,20 +0,0 @@
|
|||||||
import type { LyricLine } from '@applemusic-like-lyrics/core';
|
|
||||||
import type { ScriptItem } from '$lib/lyrics/LRCparser';
|
|
||||||
|
|
||||||
export default function mapLRCtoAMLL(line: ScriptItem, i: number, lines: ScriptItem[]): LyricLine {
|
|
||||||
return {
|
|
||||||
words: [
|
|
||||||
{
|
|
||||||
word: line.text,
|
|
||||||
startTime: line.start * 1000,
|
|
||||||
endTime: line.end * 1000
|
|
||||||
}
|
|
||||||
],
|
|
||||||
startTime: line.start * 1000,
|
|
||||||
endTime: line.end * 1000,
|
|
||||||
translatedLyric: line.translation ?? "",
|
|
||||||
romanLyric: '',
|
|
||||||
isBG: false,
|
|
||||||
isDuet: false
|
|
||||||
};
|
|
||||||
}
|
|
@ -20,7 +20,7 @@ import type { LrcJsonData, ParsedLrc, ScriptItem, ScriptWordsItem } from '../typ
|
|||||||
import type { IDTag } from './type';
|
import type { IDTag } from './type';
|
||||||
|
|
||||||
|
|
||||||
interface ParserScriptItem {
|
export interface ParserScriptItem {
|
||||||
start: number;
|
start: number;
|
||||||
text: string;
|
text: string;
|
||||||
words?: ScriptWordsItem[];
|
words?: ScriptWordsItem[];
|
||||||
@ -251,20 +251,35 @@ export default function lrcParser(lrc: string): LrcJsonData {
|
|||||||
if (parsedLrc.scripts === undefined) {
|
if (parsedLrc.scripts === undefined) {
|
||||||
return parsedLrc as LrcJsonData;
|
return parsedLrc as LrcJsonData;
|
||||||
}
|
}
|
||||||
|
|
||||||
let finalLrc: LrcJsonData = parsedLrc as LrcJsonData;
|
let finalLrc: LrcJsonData = parsedLrc as LrcJsonData;
|
||||||
let lyrics: ScriptItem[] = [];
|
let lyrics: ScriptItem[] = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
while (i < parsedLrc.scripts.length - 1) {
|
while (i < parsedLrc.scripts.length - 1) {
|
||||||
let lyricLine = parsedLrc.scripts[i] as ScriptItem;
|
let lyricLine = parsedLrc.scripts[i] as ScriptItem;
|
||||||
lyricLine.start/=1000;
|
lyricLine.start /= 1000;
|
||||||
lyricLine.end = parsedLrc.scripts[i+1].start / 1000;
|
lyricLine.end = parsedLrc.scripts[i + 1].start / 1000;
|
||||||
if (parsedLrc.scripts[i+1].text.trim() === "") {
|
|
||||||
i+=2;
|
if (parsedLrc.scripts[i + 1].text.trim() === '') {
|
||||||
} else i++;
|
i += 2;
|
||||||
if (lyricLine.text.trim() !== "") {
|
} else {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lyricLine.text.trim() !== '') {
|
||||||
lyrics.push(lyricLine);
|
lyrics.push(lyricLine);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理最后一行歌词
|
||||||
|
const lastLyric = parsedLrc.scripts[parsedLrc.scripts.length - 1] as ScriptItem;
|
||||||
|
lastLyric.start /= 1000;
|
||||||
|
if (lastLyric.text.trim() !== '') {
|
||||||
|
lastLyric.end = lastLyric.start + 3; // 设置一个默认的时长
|
||||||
|
lyrics.push(lastLyric);
|
||||||
|
}
|
||||||
|
|
||||||
finalLrc.scripts = lyrics;
|
finalLrc.scripts = lyrics;
|
||||||
return finalLrc;
|
return finalLrc;
|
||||||
}
|
}
|
||||||
|
13
src/routes/+error.svelte
Normal file
13
src/routes/+error.svelte
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<script>
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="w=screen h-screen flex flex-col justify-center items-center text-center font-sans">
|
||||||
|
<h1 style="font-size: 6.5rem; line-height: 9rem; font-weight: 200;">错误</h1>
|
||||||
|
{#if $page.status === 404}
|
||||||
|
<p style="font-size: 1.5rem; font-weight: 300">{$page.status}: 页面不存在 </p>
|
||||||
|
{:else}
|
||||||
|
<p style="font-size: 1.5rem; font-weight: 300">{$page.status}: {$page.error.message}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
@ -1 +0,0 @@
|
|||||||
export const ssr = false;
|
|
@ -1,97 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import extractFileName from '$lib/utils/extractFileName';
|
import { browser } from '$app/environment';
|
||||||
import getVersion from '$lib/utils/getVersion';
|
|
||||||
import toHumanSize from '$lib/utils/humanSize';
|
|
||||||
import localforage from '$lib/utils/storage';
|
|
||||||
interface Song {
|
|
||||||
name: string;
|
|
||||||
singer?: string;
|
|
||||||
coverUrl?: string;
|
|
||||||
size?: number;
|
|
||||||
}
|
|
||||||
interface SongList {
|
|
||||||
[key: string]: Song;
|
|
||||||
}
|
|
||||||
let musicList: SongList = {};
|
|
||||||
let idList: string[] = [];
|
|
||||||
|
|
||||||
function extractId(key: string) {
|
if (browser) {
|
||||||
const addons = ['-cover-cache', '-file', '-lyric', '-metadata', '-cover'];
|
window.location.href = '/home';
|
||||||
let r = key;
|
|
||||||
for (const addon of addons) {
|
|
||||||
if (r.endsWith(addon)) {
|
|
||||||
return [r.substring(0, r.length - addon.length), addon.replace(/-/g, ' ').trim()];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return [r, ''];
|
|
||||||
}
|
|
||||||
localforage.iterate(function (value: File | Blob | any, key, iterationNumber) {
|
|
||||||
const [id, type] = extractId(key);
|
|
||||||
if (!type) return;
|
|
||||||
if (!musicList[id]) musicList[id] = { name: '' };
|
|
||||||
if (type === 'file') {
|
|
||||||
const v = value as File;
|
|
||||||
musicList[id].name = extractFileName(v.name);
|
|
||||||
musicList[id].size = v.size;
|
|
||||||
} else if (type === 'cover') {
|
|
||||||
const v = value as Blob;
|
|
||||||
musicList[id].coverUrl = URL.createObjectURL(v);
|
|
||||||
}
|
|
||||||
idList = Object.keys(musicList);
|
|
||||||
});
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
localforage.clear();
|
|
||||||
window.location.reload();
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Aquavox - 音乐库</title>
|
<title>AquaVox</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<div
|
<div class="w-screen h-screen bg-white dark:bg-[#1f1f1f] flex flex-col justify-center items-center px-[3rem]">
|
||||||
class="absolute w-screen md:w-2/3 lg:w-1/2 left-0 md:left-[16.6667%] lg:left-1/4 px-[3%] md:px-0 top-16"
|
<span class="text-4xl font-sans">LOADING</span><br />
|
||||||
>
|
<div class="text-3xl">
|
||||||
<h1>AquaVox</h1>
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g stroke="currentColor"><circle cx="12" cy="12" r="9.5" fill="none" stroke-linecap="round" stroke-width="3"><animate attributeName="stroke-dasharray" calcMode="spline" dur="1.5s" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" keyTimes="0;0.475;0.95;1" repeatCount="indefinite" values="0 150;42 150;42 150;42 150"/><animate attributeName="stroke-dashoffset" calcMode="spline" dur="1.5s" keySplines="0.42,0,0.58,1;0.42,0,0.58,1;0.42,0,0.58,1" keyTimes="0;0.475;0.95;1" repeatCount="indefinite" values="0;-16;-59;-59"/></circle><animateTransform attributeName="transform" dur="2s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></g></svg>
|
||||||
<h2>音乐库</h2>
|
|
||||||
<div>
|
|
||||||
<ul class="mt-4 relative w-full">
|
|
||||||
{#each idList as id}
|
|
||||||
<a class="!no-underline !text-black dark:!text-white" href={`/play/${id}`}>
|
|
||||||
<li
|
|
||||||
class="relative my-4 p-4 duration-150 bg-zinc-200 hover:bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600 rounded-lg"
|
|
||||||
>
|
|
||||||
<span class="font-bold">{musicList[id].name}</span> <br />
|
|
||||||
<span>{toHumanSize(musicList[id].size)}</span> ·
|
|
||||||
<a class="!no-underline" href={`/import/${id}/lyric`}>导入歌词</a>
|
|
||||||
{#if musicList[id].coverUrl}
|
|
||||||
<img
|
|
||||||
class="h-16 w-16 object-cover absolute rounded-lg right-2 top-2"
|
|
||||||
src={musicList[id].coverUrl}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</li>
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<p>
|
|
||||||
AquaVox {getVersion()} · 早期公开预览 · 源代码参见
|
|
||||||
<a href="https://github.com/alikia2x/aquavox">GitHub</a>
|
|
||||||
</p>
|
|
||||||
<a href="/import">导入音乐</a> <br />
|
|
||||||
<button
|
|
||||||
on:click={() => window.confirm('确定要删除本地数据库中所有内容吗?') && clear()}
|
|
||||||
class="text-white bg-red-500 px-4 py-2 mt-4 rounded-md">一键清除</button
|
|
||||||
>
|
|
||||||
<h2 class="mt-4"><a href="/database/">音乐数据库</a></h2>
|
|
||||||
<p>你可以在这里探索,提交和分享好听的歌曲。</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="postcss">
|
</div>
|
||||||
a {
|
</div>
|
||||||
@apply text-red-500 hover:text-red-400 duration-150 underline;
|
|
||||||
}
|
|
||||||
</style>
|
|
1
src/routes/home/+page.server.js
Normal file
1
src/routes/home/+page.server.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const ssr = false;
|
100
src/routes/home/+page.svelte
Normal file
100
src/routes/home/+page.svelte
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import extractFileName from '$lib/utils/extractFileName';
|
||||||
|
import getVersion from '$lib/utils/getVersion';
|
||||||
|
import toHumanSize from '$lib/utils/humanSize';
|
||||||
|
import localforage from '$lib/utils/storage';
|
||||||
|
import HeadBar from '$lib/components/homepage/headBar.svelte';
|
||||||
|
interface Song {
|
||||||
|
name: string;
|
||||||
|
singer?: string;
|
||||||
|
coverUrl?: string;
|
||||||
|
size?: number;
|
||||||
|
}
|
||||||
|
interface SongList {
|
||||||
|
[key: string]: Song;
|
||||||
|
}
|
||||||
|
let musicList: SongList = {};
|
||||||
|
let idList: string[] = [];
|
||||||
|
|
||||||
|
function extractId(key: string) {
|
||||||
|
const addons = ['-cover-cache', '-file', '-lyric', '-metadata', '-cover'];
|
||||||
|
let r = key;
|
||||||
|
for (const addon of addons) {
|
||||||
|
if (r.endsWith(addon)) {
|
||||||
|
return [r.substring(0, r.length - addon.length), addon.replace(/-/g, ' ').trim()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [r, ''];
|
||||||
|
}
|
||||||
|
localforage.iterate(function (value: File | Blob | any, key, _iterationNumber) {
|
||||||
|
const [id, type] = extractId(key);
|
||||||
|
if (!type) return;
|
||||||
|
if (!musicList[id]) musicList[id] = { name: '' };
|
||||||
|
if (type === 'file') {
|
||||||
|
const v = value as File;
|
||||||
|
musicList[id].name = extractFileName(v.name);
|
||||||
|
musicList[id].size = v.size;
|
||||||
|
} else if (type === 'cover') {
|
||||||
|
const v = value as Blob;
|
||||||
|
musicList[id].coverUrl = URL.createObjectURL(v);
|
||||||
|
}
|
||||||
|
idList = Object.keys(musicList);
|
||||||
|
});
|
||||||
|
|
||||||
|
function clear() {
|
||||||
|
localforage.clear();
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>AquaVox - 首页</title>
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<HeadBar/>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="absolute w-screen md:w-2/3 lg:w-1/2 left-0 md:left-[16.6667%] lg:left-1/4 px-[3%] md:px-0 top-16"
|
||||||
|
>
|
||||||
|
<h1>AquaVox</h1>
|
||||||
|
<h2>音乐库</h2>
|
||||||
|
<div>
|
||||||
|
<div class="mt-4 relative w-full">
|
||||||
|
{#each idList as id}
|
||||||
|
<a class="!no-underline !text-black dark:!text-white" href={`/play/${id}`}>
|
||||||
|
<div
|
||||||
|
class="relative my-4 p-4 duration-150 bg-zinc-200 hover:bg-zinc-300 dark:bg-zinc-700 dark:hover:bg-zinc-600 rounded-lg"
|
||||||
|
>
|
||||||
|
<span class="font-bold">{musicList[id].name}</span> <br />
|
||||||
|
<span>{toHumanSize(musicList[id].size)}</span> ·
|
||||||
|
<a class="!no-underline" href={`/import/${id}/lyric`}>导入歌词</a>
|
||||||
|
{#if musicList[id].coverUrl}
|
||||||
|
<img
|
||||||
|
class="h-16 w-16 object-cover absolute rounded-lg right-2 top-2"
|
||||||
|
src={musicList[id].coverUrl}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
AquaVox {getVersion()} · 早期公开预览 · 源代码参见
|
||||||
|
<a href="https://github.com/alikia2x/aquavox">GitHub</a>
|
||||||
|
</p>
|
||||||
|
<a href="/import">导入音乐</a> <br />
|
||||||
|
<button
|
||||||
|
on:click={() => window.confirm('确定要删除本地数据库中所有内容吗?') && clear()}
|
||||||
|
class="text-white bg-red-500 px-4 py-2 mt-4 rounded-md">一键清除</button
|
||||||
|
>
|
||||||
|
<h2 class="mt-4"><a href="/database/">音乐数据库</a></h2>
|
||||||
|
<p>你可以在这里探索,提交和分享好听的歌曲。</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="postcss">
|
||||||
|
a {
|
||||||
|
@apply text-red-500 hover:text-red-400 duration-150 underline;
|
||||||
|
}
|
||||||
|
</style>
|
@ -13,7 +13,7 @@
|
|||||||
import type { IAudioMetadata } from 'music-metadata-browser';
|
import type { IAudioMetadata } from 'music-metadata-browser';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import progressBarRaw from '$lib/state/progressBarRaw';
|
import progressBarRaw from '$lib/state/progressBarRaw';
|
||||||
import { parseTTML, type LyricLine } from '$lib/lyrics/ttml';
|
import { parseTTML } from '$lib/lyrics/ttml';
|
||||||
import NewLyrics from '$lib/components/lyrics/newLyrics.svelte';
|
import NewLyrics from '$lib/components/lyrics/newLyrics.svelte';
|
||||||
|
|
||||||
const audioId = $page.params.id;
|
const audioId = $page.params.id;
|
||||||
@ -79,14 +79,14 @@
|
|||||||
singer = metadata.common.artist ? metadata.common.artist : '未知歌手';
|
singer = metadata.common.artist ? metadata.common.artist : '未知歌手';
|
||||||
prepared.push('duration');
|
prepared.push('duration');
|
||||||
});
|
});
|
||||||
localforage.getItem(`${audioId}-cover`, function (err, file) {
|
localforage.getItem(`${audioId}-cover`, function (_, file) {
|
||||||
if (file) {
|
if (file) {
|
||||||
const path = URL.createObjectURL(file as File);
|
const path = URL.createObjectURL(file as File);
|
||||||
coverPath.set(path);
|
coverPath.set(path);
|
||||||
}
|
}
|
||||||
prepared.push('cover');
|
prepared.push('cover');
|
||||||
});
|
});
|
||||||
localforage.getItem(`${audioId}-file`, function (err, file) {
|
localforage.getItem(`${audioId}-file`, function (_, file) {
|
||||||
if (audioPlayer === null) return;
|
if (audioPlayer === null) return;
|
||||||
if (file) {
|
if (file) {
|
||||||
const f = file as File;
|
const f = file as File;
|
||||||
@ -97,7 +97,7 @@
|
|||||||
prepared.push('file');
|
prepared.push('file');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
localforage.getItem(`${audioId}-lyric`, function (err, file) {
|
localforage.getItem(`${audioId}-lyric`, function (_, file) {
|
||||||
if (file) {
|
if (file) {
|
||||||
const f = file as File;
|
const f = file as File;
|
||||||
f.text().then((lr) => {
|
f.text().then((lr) => {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import { parseLRC } from '$lib/lyrics/lrc/parser';
|
import lrcParser, { parseLRC } from '$lib/lyrics/lrc/parser';
|
||||||
|
|
||||||
describe('LRC parser test', () => {
|
describe('LRC parser test', () => {
|
||||||
const test01Buffer = fs.readFileSync('./src/test/resources/test-01.lrc');
|
const test01Buffer = fs.readFileSync('./src/test/resources/test-01.lrc');
|
||||||
@ -9,6 +9,8 @@ describe('LRC parser test', () => {
|
|||||||
const test02Text = test02Buffer.toString('utf-8');
|
const test02Text = test02Buffer.toString('utf-8');
|
||||||
const test03Buffer = fs.readFileSync('./src/test/resources/test-03.lrc');
|
const test03Buffer = fs.readFileSync('./src/test/resources/test-03.lrc');
|
||||||
const test03Text = test03Buffer.toString('utf-8');
|
const test03Text = test03Buffer.toString('utf-8');
|
||||||
|
const test04Buffer = fs.readFileSync('./src/test/resources/test-04.lrc');
|
||||||
|
const test04Text = test04Buffer.toString('utf-8');
|
||||||
|
|
||||||
const lf_alternatives = ['\n', '\r\n', '\r'];
|
const lf_alternatives = ['\n', '\r\n', '\r'];
|
||||||
|
|
||||||
@ -37,14 +39,18 @@ describe('LRC parser test', () => {
|
|||||||
expect(result.scripts!![0].words!![1].beginIndex).toBe("[00:00.00] <00:00.04> When <00:00.16> the".indexOf("the"));
|
expect(result.scripts!![0].words!![1].beginIndex).toBe("[00:00.00] <00:00.04> When <00:00.16> the".indexOf("the"));
|
||||||
expect(result.scripts!![0].words!![1].start).toBe(160);
|
expect(result.scripts!![0].words!![1].start).toBe(160);
|
||||||
});
|
});
|
||||||
|
it('Parses test-04.lrc', () => {
|
||||||
|
const result = lrcParser(test04Text);
|
||||||
|
expect(result.scripts!![48].text).toBe("迎来每个人的结局");
|
||||||
|
});
|
||||||
it('Parses test-03.lrc', () => {
|
it('Parses test-03.lrc', () => {
|
||||||
const result = parseLRC(test03Text, { wordDiv: ' ', strict: true });
|
const result = lrcParser(test03Text);
|
||||||
expect(result.scripts!![5].text).toBe("བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ།");
|
expect(result.scripts!![5].text).toBe("བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ།");
|
||||||
expect(result.scripts!![5].translation).toBe("在舞池里舞一舞");
|
expect(result.scripts!![5].translation).toBe("在舞池里舞一舞");
|
||||||
expect(result.scripts!![6].text).toBe("祝祷转过千年 五色经幡飘飞");
|
expect(result.scripts!![6].text).toBe("祝祷转过千年 五色经幡飘飞");
|
||||||
expect(result.scripts!![6].singer).toBe("a");
|
|
||||||
expect(result.scripts!![11].singer).toBeUndefined();
|
expect(result.scripts!![11].singer).toBeUndefined();
|
||||||
expect(result.scripts!![11].translation).toBe("我们在此相聚");
|
expect(result.scripts!![11].translation).toBe("我们在此相聚");
|
||||||
|
expect(result.scripts!![70].translation).toBe("祝愿平安富足");
|
||||||
});
|
});
|
||||||
it('Rejects some invalid LRCs', () => {
|
it('Rejects some invalid LRCs', () => {
|
||||||
const cases = [
|
const cases = [
|
||||||
|
@ -3,75 +3,75 @@
|
|||||||
[al: 游四方]
|
[al: 游四方]
|
||||||
[tool: 歌词滚动姬 https://lrc-maker.github.io]
|
[tool: 歌词滚动姬 https://lrc-maker.github.io]
|
||||||
[length: 04:17.400]
|
[length: 04:17.400]
|
||||||
[00:34.280] 浸透了经卷 记忆的呼喊
|
[00:33.880] 浸透了经卷 记忆的呼喊
|
||||||
[00:37.800] 雪珠滚落山巅 栽下一个春天
|
[00:37.400] 雪珠滚落山巅 栽下一个春天
|
||||||
[00:47.390] 松石敲响玲珑清脆的银花
|
[00:46.990] 松石敲响玲珑清脆的银花
|
||||||
[00:51.600] 穿过玛瑙的红霞
|
[00:51.200] 穿过玛瑙的红霞
|
||||||
[00:54.430] 在她眼中结编 亘久诗篇
|
[00:54.030] 在她眼中结编 亘久诗篇
|
||||||
[01:05.440] a: བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ། | 在舞池里舞一舞
|
[01:05.040] བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ། | 在舞池里舞一舞
|
||||||
[01:08.780] a: 祝祷转过千年 五色经幡飘飞
|
[01:08.380] 祝祷转过千年 五色经幡飘飞
|
||||||
[01:12.040] 奏起悠扬巴叶 任岁月拨弦
|
[01:11.640] 奏起悠扬巴叶 任岁月拨弦
|
||||||
[01:19.130] གཞས་ར་འདི་ལ་གཞས་གཅིག་བཏང་། 我在歌坛献首歌
|
[01:18.730] གཞས་ར་འདི་ལ་གཞས་གཅིག་བཏང་། | 我在歌坛献首歌
|
||||||
[01:22.330] 宫殿 塔尖 彩绘 日月 同辉
|
[01:21.930] 宫殿 塔尖 彩绘 日月 同辉
|
||||||
[01:25.810] 那层厚重壁垒化身 蝉翼一片
|
[01:25.410] 那层厚重壁垒化身 蝉翼一片
|
||||||
[01:29.110] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
[01:28.710] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[01:30.790] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[01:30.390] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[01:32.510] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[01:32.110] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[01:34.120] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[01:33.720] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[01:35.920] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[01:35.520] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[01:37.630] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[01:37.230] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[01:39.350] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[01:38.950] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[01:41.050] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[01:40.650] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[01:42.740] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[01:42.340] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[01:44.630] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[01:44.230] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[01:46.280] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[01:45.880] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[01:48.010] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[01:47.610] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[01:49.600] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[01:49.200] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[01:51.380] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[01:50.980] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[01:53.070] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[01:52.670] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[01:54.820] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[01:54.420] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[01:58.580] སྔོན་དང་པོ་གྲུབ་ཐོབ་ཐང་སྟོང་རྒྱལ་པོས་མཛད་པའི་མཛད་ཚུལ་དུ། དང་པོ་རྔོན་པའི་ས་སྦྱངས་ས་འདུལ། གཉིས་པ་རྒྱ་ལུའི་བྱིན་འབེབས། གསུམ་པ་ལྷ་མོའི་གླུ་གར་སོགས་རིན་ཆེན་གསུང་མགུར་གཞུང་བཟང་མང་པོ་འདུག་སྟེ། དེ་ཡང་མ་ཉུང་གི་ཚིག་ལ་དུམ་མཚམས་གཅིག་ཞུས་པ་བྱུང་བ་ཡིན་པ་ལགས་སོ། 如祖师唐东杰布所著,一有温巴净地,二有甲鲁祈福,三有仙女歌舞,所著繁多,在此简略献之。
|
[01:58.180] སྔོན་དང་པོ་གྲུབ་ཐོབ་ཐང་སྟོང་རྒྱལ་པོས་མཛད་པའི་མཛད་ཚུལ་དུ། དང་པོ་རྔོན་པའི་ས་སྦྱངས་ས་འདུལ། གཉིས་པ་རྒྱ་ལུའི་བྱིན་འབེབས། གསུམ་པ་ལྷ་མོའི་གླུ་གར་སོགས་རིན་ཆེན་གསུང་མགུར་གཞུང་བཟང་མང་པོ་འདུག་སྟེ། དེ་ཡང་མ་ཉུང་གི་ཚིག་ལ་དུམ་མཚམས་གཅིག་ཞུས་པ་བྱུང་བ་ཡིན་པ་ལགས་སོ། | 如祖师唐东杰布所著,一有温巴净地,二有甲鲁祈福,三有仙女歌舞,所著繁多,在此简略献之。
|
||||||
[02:24.240] 浸透了经卷 记忆的呼喊
|
[02:23.840] 浸透了经卷 记忆的呼喊
|
||||||
[02:27.450] 雪珠滚落山巅 栽下一个春天
|
[02:27.050] 雪珠滚落山巅 栽下一个春天
|
||||||
[02:37.090] 松石敲响玲珑清脆的银花
|
[02:36.690] 松石敲响玲珑清脆的银花
|
||||||
[02:41.280] 穿过玛瑙的红霞
|
[02:40.880] 穿过玛瑙的红霞
|
||||||
[02:44.010] 在她眼中结编 亘久诗篇
|
[02:43.610] 在她眼中结编 亘久诗篇
|
||||||
[02:55.250] བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ། 在舞池里舞一舞
|
[02:54.850] བྲོ་ར་འདི་ལ་བྲོ་ཅིག་འཁྲབ། | 在舞池里舞一舞
|
||||||
[02:58.410] 祝祷转过千年 五色经幡飘飞
|
[02:58.010] 祝祷转过千年 五色经幡飘飞
|
||||||
[03:01.750] 奏起悠扬巴叶 任岁月拨弦
|
[03:01.350] 奏起悠扬巴叶 任岁月拨弦
|
||||||
[03:08.840] གཞས་ར་འདི་ལ་གཞས་གཅིག་བཏང་། 我在歌坛献首歌
|
[03:08.440] གཞས་ར་འདི་ལ་གཞས་གཅིག་བཏང་། | 我在歌坛献首歌
|
||||||
[03:12.050] 宫殿 塔尖 彩绘 日月 同辉
|
[03:11.650] 宫殿 塔尖 彩绘 日月 同辉
|
||||||
[03:15.400] 那层厚重壁垒化身 蝉翼一片
|
[03:15.000] 那层厚重壁垒化身 蝉翼一片
|
||||||
[03:18.850] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[03:18.450] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[03:20.480] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[03:20.080] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[03:22.210] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[03:21.810] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[03:23.910] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[03:23.510] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[03:25.662] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[03:25.262] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[03:27.391] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[03:26.991] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[03:29.096] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[03:28.696] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[03:30.789] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[03:30.389] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[03:32.496] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[03:32.096] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[03:34.175] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[03:33.775] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[03:35.876] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[03:35.476] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[03:37.606] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[03:37.206] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[03:39.290] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[03:38.890] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[03:41.030] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[03:40.630] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[03:42.679] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[03:42.279] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[03:44.455] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[03:44.055] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[03:46.176] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[03:45.776] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[03:47.910] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[03:47.510] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[03:49.625] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[03:49.225] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[03:51.293] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[03:50.893] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[03:53.005] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[03:52.605] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[03:54.742] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[03:54.342] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[03:56.479] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[03:56.079] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[03:58.159] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[03:57.759] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[03:59.859] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[03:59.459] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[04:01.548] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[04:01.148] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[04:03.312] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[04:02.912] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[04:05.026] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[04:04.626] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[04:06.721] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། 我们在此相聚
|
[04:06.321] ང་ཚོ་འདི་ལ་འཛོམས་འཛོམས། | 我们在此相聚
|
||||||
[04:08.479] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། 希望可以常聚
|
[04:08.079] གཏན་དུ་འཛོམས་རྒྱུ་བྱུང་ན། | 希望可以常聚
|
||||||
[04:10.175] གཏན་དུ་འཛོམས་པའི་མི་ལ། 在此相聚的人们
|
[04:09.775] གཏན་དུ་འཛོམས་པའི་མི་ལ། | 在此相聚的人们
|
||||||
[04:11.923] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། 祝愿平安富足
|
[04:11.523] སྙུན་གཞི་གོད་ཆགས་མ་གཏོང༌། | 祝愿平安富足
|
||||||
[04:17.400]
|
[04:17.000]
|
49
src/test/resources/test-04.lrc
Normal file
49
src/test/resources/test-04.lrc
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
[00:13.07]蝉时雨 化成淡墨渲染暮色
|
||||||
|
[00:17.84]渗透着 勾勒出足迹与车辙
|
||||||
|
[00:22.99]欢笑声 与漂浮的水汽饱和
|
||||||
|
[00:28.01]隔着窗 同城市一并模糊了
|
||||||
|
[00:33.02]拨弄着 旧吉他 哼着四拍子的歌
|
||||||
|
[00:38.18]回音中 一个人 仿佛颇悠然自得
|
||||||
|
[00:43.11]等凉雨 的温度 将不安燥热中和
|
||||||
|
[00:48.15]寻觅着 风的波折
|
||||||
|
[00:52.84]我仍然在无人问津的阴雨霉湿之地
|
||||||
|
[00:57.55]和着雨音 唱着没有听众的歌曲
|
||||||
|
[01:02.87]人潮仍是漫无目的地向目的地散去
|
||||||
|
[01:08.34]忙碌着 无为着 继续
|
||||||
|
[01:13.10]等待着谁能够将我的心房轻轻叩击
|
||||||
|
[01:17.95]即使是你 也仅仅驻足了片刻便离去
|
||||||
|
[01:22.96]想着或许 下个路口会有谁与我相遇
|
||||||
|
[01:28.52]哪怕只 一瞬的 奇迹
|
||||||
|
[01:55.08]夏夜空 出现在遥远的的记忆
|
||||||
|
[02:00.01]绽放的 璀璨花火拥着繁星
|
||||||
|
[02:05.10]消失前 做出最温柔的给予
|
||||||
|
[02:10.21]一如那些模糊身影的别离
|
||||||
|
[02:15.19]困惑地 拘束着 如城市池中之鱼
|
||||||
|
[02:20.39]或哽咽 或低泣 都融进了泡沫里
|
||||||
|
[02:25.37]拖曳疲惫身躯 沉入冰冷的池底
|
||||||
|
[02:30.48]注视着 色彩褪去
|
||||||
|
[02:34.86]我仍然在无人问津的阴雨霉湿之地
|
||||||
|
[02:39.55]和着雨音 唱着没有听众的歌曲
|
||||||
|
[02:44.90]人潮仍是漫无目的地向目的地散去
|
||||||
|
[02:50.56]忙碌着 无为着 继续
|
||||||
|
[02:55.19]祈求着谁能够将我的心房轻轻叩击
|
||||||
|
[03:00.02]今天的你 是否会留意并转身去靠近
|
||||||
|
[03:05.13]因为或许 下个路口仍是同样的结局
|
||||||
|
[03:10.90]不存在 刹那的 奇迹
|
||||||
|
[03:17.37]极夜与永昼
|
||||||
|
[03:22.38]别离与欢聚
|
||||||
|
[03:27.43]脉搏与呼吸
|
||||||
|
[03:32.45]找寻着意义
|
||||||
|
[03:38.18]我仍然在无人问津的阴雨霉湿之地
|
||||||
|
[03:43.16]和着雨音 唱着卖不出去的歌曲
|
||||||
|
[03:48.47]浮游之人也挣扎不已执着存在下去
|
||||||
|
[03:54.04]追逐着 梦想着 继续
|
||||||
|
[03:58.34]请别让我独自匍匐于滂沱世末之雨
|
||||||
|
[04:03.53]和着雨音 唱着见证终结的歌曲
|
||||||
|
[04:08.69]人们终于 结束了寻觅呆滞伫立原地
|
||||||
|
[04:14.31]哭泣着 乞求着 奇迹
|
||||||
|
[04:18.86]用这双手 拨出残缺染了锈迹的弦音
|
||||||
|
[04:23.70]都隐没于淋漓的雨幕无声无息
|
||||||
|
[04:28.89]曲终之时 你是否便会回应我的心音
|
||||||
|
[04:34.73]将颤抖的双手牵起
|
||||||
|
[04:39.80]迎来每个人的结局
|
Loading…
Reference in New Issue
Block a user