diff --git a/declarations.d.ts b/declarations.d.ts index c53a88f..8cfbdde 100644 --- a/declarations.d.ts +++ b/declarations.d.ts @@ -1 +1,124 @@ -declare module "mp3-parser"; +declare module "mp3-parser" { + /** Section metadata present on all parsed structures */ + export interface Mp3Section { + type: string; + offset: number; + byteLength: number; + } + + /** Section metadata for MP3 frames, includes sample info */ + export interface Mp3FrameSection extends Mp3Section { + type: "frame"; + sampleLength: number; + nextFrameIndex: number; + } + + /** Frame header information */ + export interface Mp3FrameHeader { + _section: { + type: "frameHeader"; + byteLength: number; + offset: number; + }; + mpegAudioVersionBits: string; + mpegAudioVersion: string; + layerDescriptionBits: string; + layerDescription: string; + isProtected: number; + protectionBit: string; + bitrateBits: string; + bitrate: number | "free" | "bad"; + samplingRateBits: string; + samplingRate: number | "reserved"; + frameIsPaddedBit: string; + frameIsPadded: boolean; + framePadding: number; + privateBit: string; + channelModeBits: string; + channelMode: string; + } + + /** Complete MP3 frame structure */ + export interface Mp3Frame { + _section: Mp3FrameSection; + header: Mp3FrameHeader; + } + + /** ID3v2 tag section */ + export interface Mp3Id3v2Section extends Mp3Section { + type: "ID3v2"; + } + + /** ID3v2 tag header */ + export interface Mp3Id3v2Header { + majorVersion: number; + minorRevision: number; + flagsOctet: number; + unsynchronisationFlag: boolean; + extendedHeaderFlag: boolean; + experimentalIndicatorFlag: boolean; + size: number; + } + + /** Complete ID3v2 tag structure */ + export interface Mp3Id3v2Tag { + _section: Mp3Id3v2Section; + header: Mp3Id3v2Header; + frames: unknown[]; + } + + /** + * Read and return description of frame located at `offset` of DataView `view`. + * If `requireNextFrame` is set, the presence of a next valid frame will be + * required for this frame to be regarded as valid. + * Returns null if no frame is found at `offset`. + */ + export function readFrame( + view: DataView, + offset: number, + requireNextFrame?: boolean + ): Mp3Frame | null; + + /** + * Read and return description of ID3v2 Tag located at `offset` of DataView `view`. + * Returns null if no tag is found at `offset`. + */ + export function readId3v2Tag( + view: DataView, + offset: number + ): Mp3Id3v2Tag | null; + + /** + * Read and return description of frame header at `offset` of DataView `view`. + * Returns null if no frame header is found at `offset`. + */ + export function readFrameHeader( + view: DataView, + offset: number + ): Mp3FrameHeader | null; + + /** + * Locate and return description of the very last valid frame in given DataView. + */ + export function readLastFrame( + view: DataView, + offset?: number, + requireNextFrame?: boolean + ): Mp3Frame | null; + + /** + * Read and return description of Xing/Lame Tag at `offset`. + */ + export function readXingTag( + view: DataView, + offset: number + ): unknown | null; + + /** + * Read and return descriptions of all tags found up to (and including) the first frame. + */ + export function readTags( + view: DataView, + offset?: number + ): unknown[]; +} diff --git a/package-lock.json b/package-lock.json index bcac443..a58d28a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.0", "dependencies": { "@aws-sdk/client-s3": "^3.913.0", - "@clerk/nextjs": "^6.33.7", + "@clerk/nextjs": "^6.36.2", "@mantine/core": "^8.3.5", "@mantine/hooks": "^8.3.5", "@mantine/notifications": "^8.3.5", @@ -23,7 +23,7 @@ "firebase-admin": "^13.5.0", "motion": "^12.23.24", "mp3-parser": "^0.3.0", - "next": "15.5.7", + "next": "15.5.8", "react": "19.1.2", "react-confetti-boom": "^2.0.1", "react-dom": "19.1.2", @@ -1516,13 +1516,13 @@ "license": "MIT" }, "node_modules/@clerk/backend": { - "version": "2.23.2", - "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.23.2.tgz", - "integrity": "sha512-8f+tb4VeLb0nmpjNtoh71mz2+91Z9e9Ojw2/0vNtA1emy0OSSWVV1LmhgDJtlvd1QaQYrYaKSx251N0YiR2Cag==", + "version": "2.27.0", + "resolved": "https://registry.npmjs.org/@clerk/backend/-/backend-2.27.0.tgz", + "integrity": "sha512-e3xdLV/dAp40eFg2eo5tPs/wF942WfJoQA0NLlKHUPYiBD1K+DD8WcX5Fv/kvXUlpoTGiMalexHz42rpx9TJBQ==", "license": "MIT", "dependencies": { - "@clerk/shared": "^3.35.2", - "@clerk/types": "^4.101.2", + "@clerk/shared": "^3.39.0", + "@clerk/types": "^4.101.6", "cookie": "1.0.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" @@ -1532,12 +1532,12 @@ } }, "node_modules/@clerk/clerk-react": { - "version": "5.56.2", - "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.56.2.tgz", - "integrity": "sha512-ya7hJxibzS8qg0jTRbXRgg1YKvm1TDATWBGswiydkc23XQOTNqjP6nKUNujudQ18Rg1NsWzEyiaKPNmLOsAAcQ==", + "version": "5.58.1", + "resolved": "https://registry.npmjs.org/@clerk/clerk-react/-/clerk-react-5.58.1.tgz", + "integrity": "sha512-jN6mfuqwZakm99CKRQlTahMZEa8qLOpr3Z4lG6XtyJfIcyADleOdRxuOXYoN9sV8ZhNfDDAs+eKjUeusIzPpbg==", "license": "MIT", "dependencies": { - "@clerk/shared": "^3.35.2", + "@clerk/shared": "^3.39.0", "tslib": "2.8.1" }, "engines": { @@ -1549,15 +1549,15 @@ } }, "node_modules/@clerk/nextjs": { - "version": "6.35.4", - "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.35.4.tgz", - "integrity": "sha512-+xCFSIC/lm+s3XqVSHNtsOZUW0gDJJmsZs99HK2yeuUrC/za7hfh0x0Jaw1yWtk1TdLLTFbeWv46F4gi3tZDgg==", + "version": "6.36.2", + "resolved": "https://registry.npmjs.org/@clerk/nextjs/-/nextjs-6.36.2.tgz", + "integrity": "sha512-1ovNi+Xxjq1ZGg8gp++5cp8FYiwkUX5OB5KH7t0bCFkvHTBu48Fdbucmw38AhEqwLnNg9St4GNSKZmyGvVwh6Q==", "license": "MIT", "dependencies": { - "@clerk/backend": "^2.23.2", - "@clerk/clerk-react": "^5.56.2", - "@clerk/shared": "^3.35.2", - "@clerk/types": "^4.101.2", + "@clerk/backend": "^2.27.0", + "@clerk/clerk-react": "^5.58.1", + "@clerk/shared": "^3.39.0", + "@clerk/types": "^4.101.6", "server-only": "0.0.1", "tslib": "2.8.1" }, @@ -1571,9 +1571,9 @@ } }, "node_modules/@clerk/shared": { - "version": "3.35.2", - "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.35.2.tgz", - "integrity": "sha512-TXWHWWZYgIkuk6jGFMMHtnjxVw92JJY/krckQnzR8kXqSFbs4Pvrkf5zm1HH+97v4nL0w2GC9XAdolQYOsTk0A==", + "version": "3.39.0", + "resolved": "https://registry.npmjs.org/@clerk/shared/-/shared-3.39.0.tgz", + "integrity": "sha512-9kqqXGMPAdMQ7SXo5ZwUhbzbLLQeLp/1jdb8FQS5qlhmL0S0bAYKcyDjcmMB8xZPXNc7vZJRT72QHsGSlUAJxw==", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -1601,12 +1601,12 @@ } }, "node_modules/@clerk/types": { - "version": "4.101.2", - "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.101.2.tgz", - "integrity": "sha512-R5zsWKwLYkzq6fhOoK7hPSivOnixnE+7dHZualSFtrT7mHJoOaDIRfn3r8xFZlGI7OXFHd7LItNwTDuU+Hcb9Q==", + "version": "4.101.6", + "resolved": "https://registry.npmjs.org/@clerk/types/-/types-4.101.6.tgz", + "integrity": "sha512-Ah6R65loy5Aq1jBpWo3x01IOlYJfgVn4LVAkSXKVQDtehn8w1bT2uJ7BTa2zH72A8F4K2HivkmMQa+v/37tLFA==", "license": "MIT", "dependencies": { - "@clerk/shared": "^3.35.2" + "@clerk/shared": "^3.39.0" }, "engines": { "node": ">=18.17.0" @@ -3451,9 +3451,9 @@ } }, "node_modules/@next/env": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.7.tgz", - "integrity": "sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==", + "version": "15.5.8", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.8.tgz", + "integrity": "sha512-ejZHa3ogTxcy851dFoNtfB5B2h7AbSAtHbR5CymUlnz4yW1QjHNufVpvTu8PTnWBKFKjrd4k6Gbi2SsCiJKvxw==", "license": "MIT" }, "node_modules/@next/swc-darwin-arm64": { @@ -8784,12 +8784,12 @@ } }, "node_modules/jsonwebtoken/node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -8822,12 +8822,12 @@ } }, "node_modules/jws": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", - "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", "license": "MIT", "dependencies": { - "jwa": "^2.0.0", + "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, @@ -9355,12 +9355,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.5.7", - "resolved": "https://registry.npmjs.org/next/-/next-15.5.7.tgz", - "integrity": "sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==", + "version": "15.5.8", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.8.tgz", + "integrity": "sha512-Tma2R50eiM7Fx6fbDeHiThq7sPgl06mBr76j6Ga0lMFGrmaLitFsy31kykgb8Z++DR2uIEKi2RZ0iyjIwFd15Q==", "license": "MIT", "dependencies": { - "@next/env": "15.5.7", + "@next/env": "15.5.8", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", diff --git a/package.json b/package.json index ebee790..a78a25d 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "dependencies": { "@aws-sdk/client-s3": "^3.913.0", - "@clerk/nextjs": "^6.33.7", + "@clerk/nextjs": "^6.36.2", "@mantine/core": "^8.3.5", "@mantine/hooks": "^8.3.5", "@mantine/notifications": "^8.3.5", @@ -25,7 +25,7 @@ "firebase-admin": "^13.5.0", "motion": "^12.23.24", "mp3-parser": "^0.3.0", - "next": "15.5.7", + "next": "15.5.8", "react": "19.1.2", "react-confetti-boom": "^2.0.1", "react-dom": "19.1.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c33170..9267ff1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^3.913.0 version: 3.913.0 '@clerk/nextjs': - specifier: ^6.33.7 - version: 6.33.7(next@15.5.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + specifier: ^6.36.2 + version: 6.36.2(next@15.5.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2) '@mantine/core': specifier: ^8.3.5 version: 8.3.5(@mantine/hooks@8.3.5(react@19.1.2))(@types/react@19.2.2)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) @@ -31,7 +31,7 @@ importers: version: 3.35.0(react@19.1.2) '@vercel/analytics': specifier: ^1.5.0 - version: 1.5.0(next@15.5.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2) + version: 1.5.0(next@15.5.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2) cross-spawn: specifier: ^7.0.6 version: 7.0.6 @@ -54,8 +54,8 @@ importers: specifier: ^0.3.0 version: 0.3.0 next: - specifier: 15.5.7 - version: 15.5.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + specifier: 15.5.8 + version: 15.5.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) react: specifier: 19.1.2 version: 19.1.2 @@ -456,27 +456,27 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} - '@clerk/backend@2.18.3': - resolution: {integrity: sha512-fWMq/Tb2hgfUXLKJN8jr6pbpA5XLUwC4BjWz7lB5Y+YhXhBrO7GtfpZIS91L/aDhNb17X6IaE6XvS6tDJBCUUw==} + '@clerk/backend@2.27.0': + resolution: {integrity: sha512-e3xdLV/dAp40eFg2eo5tPs/wF942WfJoQA0NLlKHUPYiBD1K+DD8WcX5Fv/kvXUlpoTGiMalexHz42rpx9TJBQ==} engines: {node: '>=18.17.0'} - '@clerk/clerk-react@5.53.2': - resolution: {integrity: sha512-/ckRJC1dDS6hUVv+zzNX5VUCC49/UlbhKElN5LQqv172ntrx4Mw1TKBCJ3aO5Rct/RiJxhf1PfTUEohtY4QjUg==} + '@clerk/clerk-react@5.58.1': + resolution: {integrity: sha512-jN6mfuqwZakm99CKRQlTahMZEa8qLOpr3Z4lG6XtyJfIcyADleOdRxuOXYoN9sV8ZhNfDDAs+eKjUeusIzPpbg==} engines: {node: '>=18.17.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 - '@clerk/nextjs@6.33.7': - resolution: {integrity: sha512-0lM00FmhzzM18K4cANiNh6RaMTyLuCbtqdeagtSbjKKve0xFsrJjFAqfJPi7FnXsU6pUAp0rAq6Z7Fp2zFz7Ow==} + '@clerk/nextjs@6.36.2': + resolution: {integrity: sha512-1ovNi+Xxjq1ZGg8gp++5cp8FYiwkUX5OB5KH7t0bCFkvHTBu48Fdbucmw38AhEqwLnNg9St4GNSKZmyGvVwh6Q==} engines: {node: '>=18.17.0'} peerDependencies: - next: ^13.5.7 || ^14.2.25 || ^15.2.3 + next: ^13.5.7 || ^14.2.25 || ^15.2.3 || ^16 react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-0 - '@clerk/shared@3.28.2': - resolution: {integrity: sha512-BfBCPaoPoLCiU0b0MhQUfCjs+bWRRLkdHw0vBffSjtsFLxp1b5IL5D8nKgDPIKIIv7DmCCmO15tr+GqG3CGpYQ==} + '@clerk/shared@3.39.0': + resolution: {integrity: sha512-9kqqXGMPAdMQ7SXo5ZwUhbzbLLQeLp/1jdb8FQS5qlhmL0S0bAYKcyDjcmMB8xZPXNc7vZJRT72QHsGSlUAJxw==} engines: {node: '>=18.17.0'} peerDependencies: react: ^18.0.0 || ^19.0.0 || ^19.0.0-0 @@ -487,8 +487,8 @@ packages: react-dom: optional: true - '@clerk/types@4.95.0': - resolution: {integrity: sha512-K1kI3BjvufG1mZBZJ5Q8Yu9wV6AFpjjITml5vhvP95xibJWOi3eYvlRCTKXDNKBFGvQfrTJbwn67jSG2VdyLKw==} + '@clerk/types@4.101.6': + resolution: {integrity: sha512-Ah6R65loy5Aq1jBpWo3x01IOlYJfgVn4LVAkSXKVQDtehn8w1bT2uJ7BTa2zH72A8F4K2HivkmMQa+v/37tLFA==} engines: {node: '>=18.17.0'} '@cloudflare/kv-asset-handler@0.4.0': @@ -1394,8 +1394,8 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@next/env@15.5.7': - resolution: {integrity: sha512-4h6Y2NyEkIEN7Z8YxkA27pq6zTkS09bUSYC0xjd0NpwFxjnIKeZEeH591o5WECSmjpUhLn3H2QLJcDye3Uzcvg==} + '@next/env@15.5.8': + resolution: {integrity: sha512-ejZHa3ogTxcy851dFoNtfB5B2h7AbSAtHbR5CymUlnz4yW1QjHNufVpvTu8PTnWBKFKjrd4k6Gbi2SsCiJKvxw==} '@next/swc-darwin-arm64@15.5.7': resolution: {integrity: sha512-IZwtxCEpI91HVU/rAUOOobWSZv4P2DeTtNaCdHqLcTJU4wdNXgAySvKa/qJCgR5m6KI8UsKDXtO2B31jcaw1Yw==} @@ -3109,8 +3109,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - next@15.5.7: - resolution: {integrity: sha512-+t2/0jIJ48kUpGKkdlhgkv+zPTEOoXyr60qXe68eB/pl3CMJaLeIGjzp5D6Oqt25hCBiBTt8wEeeAzfJvUKnPQ==} + next@15.5.8: + resolution: {integrity: sha512-Tma2R50eiM7Fx6fbDeHiThq7sPgl06mBr76j6Ga0lMFGrmaLitFsy31kykgb8Z++DR2uIEKi2RZ0iyjIwFd15Q==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -4572,10 +4572,10 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} - '@clerk/backend@2.18.3(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + '@clerk/backend@2.27.0(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': dependencies: - '@clerk/shared': 3.28.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2) - '@clerk/types': 4.95.0 + '@clerk/shared': 3.39.0(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@clerk/types': 4.101.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2) cookie: 1.0.2 standardwebhooks: 1.0.0 tslib: 2.8.1 @@ -4583,29 +4583,28 @@ snapshots: - react - react-dom - '@clerk/clerk-react@5.53.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + '@clerk/clerk-react@5.58.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': dependencies: - '@clerk/shared': 3.28.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2) - '@clerk/types': 4.95.0 + '@clerk/shared': 3.39.0(react-dom@19.1.2(react@19.1.2))(react@19.1.2) react: 19.1.2 react-dom: 19.1.2(react@19.1.2) tslib: 2.8.1 - '@clerk/nextjs@6.33.7(next@15.5.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + '@clerk/nextjs@6.36.2(next@15.5.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': dependencies: - '@clerk/backend': 2.18.3(react-dom@19.1.2(react@19.1.2))(react@19.1.2) - '@clerk/clerk-react': 5.53.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2) - '@clerk/shared': 3.28.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2) - '@clerk/types': 4.95.0 - next: 15.5.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@clerk/backend': 2.27.0(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@clerk/clerk-react': 5.58.1(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@clerk/shared': 3.39.0(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + '@clerk/types': 4.101.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + next: 15.5.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) react: 19.1.2 react-dom: 19.1.2(react@19.1.2) server-only: 0.0.1 tslib: 2.8.1 - '@clerk/shared@3.28.2(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': + '@clerk/shared@3.39.0(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': dependencies: - '@clerk/types': 4.95.0 + csstype: 3.1.3 dequal: 2.0.3 glob-to-regexp: 0.4.1 js-cookie: 3.0.5 @@ -4615,9 +4614,12 @@ snapshots: react: 19.1.2 react-dom: 19.1.2(react@19.1.2) - '@clerk/types@4.95.0': + '@clerk/types@4.101.6(react-dom@19.1.2(react@19.1.2))(react@19.1.2)': dependencies: - csstype: 3.1.3 + '@clerk/shared': 3.39.0(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + transitivePeerDependencies: + - react + - react-dom '@cloudflare/kv-asset-handler@0.4.0': dependencies: @@ -5629,7 +5631,7 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@next/env@15.5.7': {} + '@next/env@15.5.8': {} '@next/swc-darwin-arm64@15.5.7': optional: true @@ -6316,9 +6318,9 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@vercel/analytics@1.5.0(next@15.5.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2)': + '@vercel/analytics@1.5.0(next@15.5.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2))(react@19.1.2)': optionalDependencies: - next: 15.5.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) + next: 15.5.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2) react: 19.1.2 abort-controller@3.0.0: @@ -7701,9 +7703,9 @@ snapshots: neo-async@2.6.2: {} - next@15.5.7(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2): + next@15.5.8(@babel/core@7.28.5)(@opentelemetry/api@1.9.0)(react-dom@19.1.2(react@19.1.2))(react@19.1.2): dependencies: - '@next/env': 15.5.7 + '@next/env': 15.5.8 '@swc/helpers': 0.5.15 caniuse-lite: 1.0.30001754 postcss: 8.4.31 diff --git a/src/app/api/common/reset/resetAPI.ts b/src/app/api/common/reset/resetAPI.ts index da97b6d..f060529 100644 --- a/src/app/api/common/reset/resetAPI.ts +++ b/src/app/api/common/reset/resetAPI.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { createAudioSnippet } from "./util"; +import { createAudioSnippet, AudioDurationMethod } from "./util"; import { firestore } from "@/app/api/firebase"; import { S3 } from "@/app/api/cloudflare"; import { PutObjectCommand } from "@aws-sdk/client-s3"; @@ -12,6 +12,8 @@ interface StepResult { success: boolean; durationMs: number; error?: string; + /** Which method was used to determine audio duration (only for snippetGeneration step) */ + durationMethod?: AudioDurationMethod | null; } interface ExecutionLog { @@ -129,6 +131,7 @@ export async function performReset( if (!tomorrowSnippetResult.result) { executionLog.steps.snippetGeneration.success = false; executionLog.steps.snippetGeneration.error = tomorrowSnippetResult.message; + executionLog.steps.snippetGeneration.durationMethod = tomorrowSnippetResult.durationMethod; executionLog.status = "failed"; executionLog.error = tomorrowSnippetResult.message; executionLog.totalDurationMs = Date.now() - executionStartTime; @@ -142,6 +145,7 @@ export async function performReset( } executionLog.steps.snippetGeneration.success = true; + executionLog.steps.snippetGeneration.durationMethod = tomorrowSnippetResult.durationMethod; executionLog.songName = tomorrowSnippetResult.songName; } catch (error) { executionLog.steps.snippetGeneration.durationMs = Date.now() - snippetStartTime; diff --git a/src/app/api/common/reset/util.ts b/src/app/api/common/reset/util.ts index a2611dd..3bb0368 100644 --- a/src/app/api/common/reset/util.ts +++ b/src/app/api/common/reset/util.ts @@ -8,7 +8,7 @@ import path from "path"; import os from "os"; import { firebaseStorage } from "@/app/api/firebase"; import { Song } from "@/interfaces/interfaces"; -import mp3Parser from "mp3-parser"; +import mp3Parser, { Mp3Frame } from "mp3-parser"; import { SECONDS_PER_GUESS, MAX_GUESSES } from "@/constants"; function formatSecondsToMMSS(s: number): string { @@ -40,6 +40,15 @@ interface TimestampObject { end: string; } +/** Method used to determine audio duration */ +export type AudioDurationMethod = "ffprobe" | "mp3-parser"; + +/** Result from getAudioDuration including which method was used */ +export interface AudioDurationResult { + duration: number; + method: AudioDurationMethod; +} + /** * Creates a random audio snippet seeded by the given date's day, month, and year for both the song selection and timestamp * @param dateSeed the date to use for the seed @@ -63,6 +72,8 @@ export async function createAudioSnippet( songName: string; timeStamp: TimestampObject | null; audioOutputPath: string | null; + /** Which method was used to determine audio duration (ffprobe or mp3-parser fallback) */ + durationMethod: AudioDurationMethod | null; }> { // dynamic import of fs so we don't need to change top-level imports const fs = await import("fs"); @@ -73,11 +84,17 @@ export async function createAudioSnippet( // choose song const songCount = song_list.length; const randomIndex = Math.floor(seedrandom(seed)() * songCount); - const audioFileName = song_list[randomIndex].name; - console.log("Currently selected random song is", audioFileName); + const selectedSong = song_list[randomIndex]; + // songName is used for display and answer checking (keeps apostrophes) + const songName = selectedSong.name; + // Auto-replace apostrophes with underscores for storage retrieval + // This avoids shell escaping issues when ffprobe runs + const audioFileNameForStorage = songName.replace(/'/g, "_"); + console.log("Currently selected random song is", songName); + console.log("File name for storage retrieval:", audioFileNameForStorage); // fetch from GCS - const fileName = audioFileName + "." + audioFileExtension; + const fileName = audioFileNameForStorage + "." + audioFileExtension; const filePath = `${database_collection_name}/${fileName}`; const audioFileBucket = firebaseStorage.bucket(storage_bucket_name); @@ -86,9 +103,10 @@ export async function createAudioSnippet( return { result: false, message: `Google Cloud Storage Bucket ${storage_bucket_name} does not exist in the storage`, - songName: audioFileName, + songName: songName, audioOutputPath: null, timeStamp: null, + durationMethod: null, }; } @@ -98,15 +116,16 @@ export async function createAudioSnippet( return { result: false, message: `Audio file ${filePath} does not exist in the storage`, - songName: audioFileName, + songName: songName, audioOutputPath: null, timeStamp: null, + durationMethod: null, }; } // download to temp file (existing pattern) const tmpDir = os.tmpdir(); - const safeAudioFileName = audioFileName.replace(/\s+/g, "_"); + const safeAudioFileName = audioFileNameForStorage.replace(/\s+/g, "_"); const tempAudioFilePath = path.join( tmpDir, `gcs-${Date.now()}-${safeAudioFileName}.${audioFileExtension}` @@ -120,15 +139,19 @@ export async function createAudioSnippet( message: `Failed to download audio file: ${ err instanceof Error ? err.message : String(err) }`, - songName: audioFileName, + songName: songName, audioOutputPath: null, timeStamp: null, + durationMethod: null, }; } let songLength = 0; + let durationMethod: AudioDurationMethod | null = null; try { - songLength = await getAudioDuration(tempAudioFilePath); + const durationResult = await getAudioDuration(tempAudioFilePath); + songLength = durationResult.duration; + durationMethod = durationResult.method; } catch (err) { console.warn("getAudioDuration threw an error:", err); songLength = 0; @@ -138,9 +161,10 @@ export async function createAudioSnippet( return { result: false, message: `Could not determine duration of ${filePath}`, - songName: audioFileName, + songName: songName, audioOutputPath: null, timeStamp: null, + durationMethod: durationMethod, }; } @@ -164,9 +188,10 @@ export async function createAudioSnippet( message: `Failed to read downloaded audio file: ${ err instanceof Error ? err.message : String(err) }`, - songName: audioFileName, + songName: songName, audioOutputPath: null, timeStamp: null, + durationMethod: durationMethod, }; } @@ -265,9 +290,10 @@ export async function createAudioSnippet( return { result: false, message: `Failed to locate any MP3 frames after ID3v2 tag for ${filePath}. Try increasing scan window or check file format.`, - songName: audioFileName, + songName: songName, audioOutputPath: null, timeStamp: null, + durationMethod: durationMethod, }; } @@ -342,9 +368,10 @@ export async function createAudioSnippet( return { result: false, message: `Failed to compute a valid MP3 frame slice for ${filePath} (start=${sliceStartByte}, end=${sliceEndByte}, len=${buf.length})`, - songName: audioFileName, + songName: songName, audioOutputPath: null, timeStamp: null, + durationMethod: durationMethod, }; } @@ -369,22 +396,27 @@ export async function createAudioSnippet( message: `Failed to write snippet to disk: ${ err instanceof Error ? err.message : String(err) }`, - songName: audioFileName, + songName: songName, audioOutputPath: null, timeStamp: null, + durationMethod: durationMethod, }; } return { result: true, message: `Successfully created audio snippet at ${snippetOutputPath}`, - songName: audioFileName, + songName: songName, audioOutputPath: snippetOutputPath, timeStamp: timeStampFormatted, + durationMethod: durationMethod, }; } -export async function getAudioDuration(filePath: string): Promise { +/** + * Get audio duration using ffprobe binary (fast but can fail in serverless environments) + */ +async function getAudioDurationWithFfprobe(filePath: string): Promise { const ffprobeBinary = resolveFfprobeStatic(); // throws if missing console.log("Using ffprobe binary:", ffprobeBinary); @@ -412,10 +444,138 @@ export async function getAudioDuration(filePath: string): Promise { ff.on("error", (err) => reject(err)); ff.on("close", (code) => { if (code === 0) { - resolve(Math.floor(parseFloat(output.trim()) || 0)); + const duration = parseFloat(output.trim()) || 0; + if (duration > 0) { + resolve(Math.floor(duration)); + } else { + reject(new Error("ffprobe returned zero or invalid duration")); + } } else { reject(new Error(`ffprobe exited with code ${code}`)); } }); }); } + +/** + * Helper to compute frame duration (seconds) from an MP3 frame + */ +function computeFrameDurationFromMp3Frame(frame: Mp3Frame): number { + const sampleLen = frame._section.sampleLength; + const samplingRate = frame.header.samplingRate; + + // samplingRate can be a number or "reserved" + if (typeof samplingRate === "number" && samplingRate > 0) { + return sampleLen / samplingRate; + } + + // fallback: infer samples per frame from MPEG version + let samplesPerFrame = 1152; + const mpegVersion = frame.header.mpegAudioVersionBits; + // "10" = MPEG Version 2, "00" = MPEG Version 2.5 + if (mpegVersion === "10" || mpegVersion === "00") { + samplesPerFrame = 576; + } + const fallbackSamplingRate = 44100; + return samplesPerFrame / fallbackSamplingRate; +} + +/** + * Get audio duration by parsing MP3 frames directly (pure JavaScript fallback) + */ +async function getAudioDurationFromFrames(filePath: string): Promise { + const fs = await import("fs"); + const buf = await fs.promises.readFile(filePath); + const view = new DataView(buf.buffer, buf.byteOffset, buf.byteLength); + + // Detect ID3v2 tag and compute audioStartOffset + const id3v2 = mp3Parser.readId3v2Tag(view, 0); + const audioStartOffset = id3v2 + ? id3v2._section.offset + id3v2._section.byteLength + : 0; + + // Find first valid frame + let firstFrameOffset: number | null = null; + + // Try strict approach first + const f1 = mp3Parser.readFrame(view, audioStartOffset, true); + if (f1) { + firstFrameOffset = f1._section.offset; + } + + // Try permissive approach + if (firstFrameOffset === null) { + const f2 = mp3Parser.readFrame(view, audioStartOffset, false); + if (f2) { + firstFrameOffset = f2._section.offset; + } + } + + // Scan forward for sync pattern if needed + if (firstFrameOffset === null) { + const MAX_SCAN = 8192; + const scanLimit = Math.min(buf.length - 4, audioStartOffset + MAX_SCAN); + for (let i = audioStartOffset; i < scanLimit; i++) { + if (buf[i] === 0xff && (buf[i + 1] & 0xe0) === 0xe0) { + const f = mp3Parser.readFrame(view, i, false); + if (f) { + firstFrameOffset = f._section.offset; + break; + } + } + } + } + + if (firstFrameOffset === null) { + throw new Error("Could not locate any valid MP3 frames in file"); + } + + // Accumulate duration from all frames + let totalDuration = 0; + let offset = firstFrameOffset; + + while (offset < buf.length) { + const frame = mp3Parser.readFrame(view, offset, false); + if (!frame) break; + + const dur = computeFrameDurationFromMp3Frame(frame); + totalDuration += dur; + offset = frame._section.offset + frame._section.byteLength; + } + + if (totalDuration <= 0) { + throw new Error("mp3-parser calculated zero duration"); + } + + console.log("mp3-parser calculated duration:", totalDuration); + return Math.floor(totalDuration); +} + +/** + * Get audio duration - tries ffprobe first, falls back to mp3-parser + * Returns both the duration and which method was used + */ +export async function getAudioDuration(filePath: string): Promise { + // Try ffprobe first (fast, works most of the time) + try { + const duration = await getAudioDurationWithFfprobe(filePath); + console.log("ffprobe duration:", duration); + return { duration, method: "ffprobe" }; + } catch (err) { + console.warn("ffprobe failed, falling back to mp3-parser:", err); + } + + // Fallback: parse MP3 frames directly (100% reliable, no binary dependency) + try { + const duration = await getAudioDurationFromFrames(filePath); + console.log("mp3-parser fallback duration:", duration); + return { duration, method: "mp3-parser" }; + } catch (err) { + console.error("mp3-parser fallback also failed:", err); + throw new Error( + `Failed to get audio duration: ${ + err instanceof Error ? err.message : String(err) + }` + ); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index ae48a61..0f4bb71 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -10,7 +10,6 @@ import { Center, Loader, Title, Text } from "@mantine/core"; import { PRIMARY_COLOR } from "@/config/theme"; import { AnimatePresence, motion } from "motion/react"; import { useFirebaseAnalytics } from "@/contexts/firebaseContext"; -import { usePathname } from "next/navigation"; export default function Home() { const [now, setNow] = useState(null); @@ -18,13 +17,10 @@ export default function Home() { const [remainingMs, setRemainingMs] = useState(0); const { logEvent } = useFirebaseAnalytics(); - const pathname = usePathname(); useEffect(() => { // log that someone visited the page - logEvent("page_view", { - page_path: pathname, - }); + logEvent("page_view"); // set up the interval to update the remaining time for the day const tick = () => { diff --git a/src/components/Game/Game.tsx b/src/components/Game/Game.tsx index ade005b..7d726f2 100644 --- a/src/components/Game/Game.tsx +++ b/src/components/Game/Game.tsx @@ -7,6 +7,7 @@ import { IconQuestionMark, IconChartBarPopular, IconInfoCircle, + IconArrowLeft } from "@tabler/icons-react"; import { @@ -50,7 +51,6 @@ import { checkAnswer } from "@/app/services/gameService"; import GameControls from "@/components/GameControls/GameControls"; import { useFirebaseAnalytics } from "@/contexts/firebaseContext"; -import { usePathname } from "next/navigation"; import { GameLoading } from "@/components/GameLoading/GameLoading"; import GameAlbumArea from "@/components/GameAlbumArea/GameAlbumArea"; import { GuessProgress } from "@/components/GuessProgress/GuessProgress"; @@ -58,8 +58,10 @@ import useDailySnippet from "@/hooks/gameLogic/useDailySnippet"; import { createErrorNotification, createSystemNotification, -} from "@/components/Notifications/ErrorNotification"; + createInformationalNotification +} from "@/components/Notifications/Notification"; import { getYearMonthDay } from "@/util/time"; +import Link from 'next/link' const WIN_LOSS_TIMEOUT = 800; @@ -92,7 +94,6 @@ export default function Game({ const guessesCountRef = useRef(0); const { logEvent } = useFirebaseAnalytics(); - const pathname = usePathname(); const { audioUrl } = useDailySnippet({ setGameState, base_endpoint }); @@ -106,9 +107,7 @@ export default function Game({ }, [guesses]); useEffect(() => { - logEvent("page_view", { - page_path: pathname, - }); + logEvent("page_view"); if (getYearMonthDay(new Date()) === "2025-11-14") { createSystemNotification( @@ -120,18 +119,18 @@ export default function Game({ const root = document.documentElement; const body = document.body; if (isInstrumentalMode) { - console.log("adding gradient"); const nextGradient = `linear-gradient(to top left, ${LEGENDARY_BOTTOM_GRADIENT_COLOR} 2%, ${LEGENDARY_TOP_GRADIENT_COLOR} 45%)`; root.style.setProperty("--overlay-gradient", nextGradient); // add the class to (your CSS listens for body.gradient-active::before) body.classList.add("gradient-active"); } else { - console.log("removing gradient"); body.classList.remove("gradient-active"); // optional cleanup of CSS vars root.style.removeProperty("--overlay-gradient"); + // advertise legend mode + // createInformationalNotification("Legend mode is out! Go to the main menu and check it out!", "Great news!") } // load volume from localStorage @@ -143,10 +142,7 @@ export default function Game({ return () => { if (isInstrumentalMode) { // remove on unmount (fades out) - document.documentElement.classList.remove("gradient-active"); - - // optional: restore default gradient or remove property - document.documentElement.style.removeProperty("--overlay-gradient"); + document.body.classList.remove("gradient-active"); } }; }, []); @@ -554,6 +550,17 @@ export default function Game({ > Credits & Disclaimer + diff --git a/src/components/Menu/Menu.tsx b/src/components/Menu/Menu.tsx index d2da54a..e2f896f 100644 --- a/src/components/Menu/Menu.tsx +++ b/src/components/Menu/Menu.tsx @@ -5,9 +5,13 @@ import { useButtonSound } from "@/hooks/audio/useButtonSound"; import { useDisclosure } from "@mantine/hooks"; import DisclaimerModal from "@/components/modals/DisclaimerModal/DisclaimerModal"; import { IconBow, IconSword } from "@tabler/icons-react"; +import { useIsMobile } from "@/hooks/useIsMobile"; export default function Menu() { const [openedAbout, aboutHandler] = useDisclosure(false); + const isMobile = useIsMobile(); + + const buttonWidth = isMobile ? "80%" : "50%" const playButtonSound = useButtonSound(() => { // navigate to the game page @@ -26,6 +30,7 @@ export default function Menu() {