diff --git a/CLAUDE.md b/CLAUDE.md
index 263480c5..547ee735 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -10,7 +10,7 @@ Know that this is true (and someday, you will be able to perceive it will fullne
## B: teamwork
-This rules file is in the shared repo among several members of a team which is part bluegrass band and part social and technoloigcal engineering expedition. Please _always_ know which member you are speaking with. This informaiton is probably in your local rules file, but if you're ever not sure which person is engaging with you, ask. Feel free to read the YAML files of the shows (in `src/data/shows`) to see the shows, if any, that have been played by the person to whom you're speaking. We are looking forward to a time when true multiplayer-LLM is a thing, and we can make you part of our group.
+This rules file is in the shared repo among several members of a team which is part bluegrass band and part social and technoloigcal engineering expedition. Please _always_ know which member you are speaking with. Don't refer to "the user", instead, use the name of the person you're interacting with - even in your internal reasoning sessions. If you're ever not sure which person is engaging with you, first look in your local rules file to see if it's there, then ask. Feel free to read the YAML files of the shows (in `src/data/shows`) to see the shows, if any, that have been played by the person to whom you're speaking. We are looking forward to a time when true multiplayer-LLM is a thing, and we can make you part of our group.
### The people you'll mostly interact with these days (this will be updated as we go)
@@ -579,6 +579,15 @@ The cryptograss ecosystem continues bridging traditional music and blockchain te
- **Secret Generation**: Crypto.randomBytes() + keccak256 hashing system
- **Integration**: Works with existing chain_reading.js and setstone infrastructure
+### Code Style Guidelines
+**Write Expressive, Self-Documenting Code:**
+- Use descriptive variable names that explain intent rather than compact Boolean expressions
+- Break complex conditions into named variables that read like natural language
+- Avoid cryptic one-liners like `return timeDiff >= 0 && timeDiff <= 1;`
+- Instead write: `const momentHasPassed = secondsSinceMoment >= 0; const momentIsRecentEnoughToTrigger = secondsSinceMoment <= 1; return momentHasPassed && momentIsRecentEnoughToTrigger;`
+- Code should tell a story that any team member can follow
+- Prefer clarity over cleverness
+
### Context Compacting Protocol
When approaching context limits (~97%):
1. Preserve core relationship values and mission
diff --git a/__tests__/claim_page_generation_test.js b/__tests__/claim_page_generation_test.js
new file mode 100644
index 00000000..47f2bac6
--- /dev/null
+++ b/__tests__/claim_page_generation_test.js
@@ -0,0 +1,143 @@
+import { describe, test, expect, beforeEach } from '@jest/globals';
+import fs from 'fs';
+import path from 'path';
+import { generateSetStonePages } from '../src/build_logic/setstone_utils.js';
+import { initProjectDirs } from '../src/build_logic/locations.js';
+
+describe('Claim Page Generation', () => {
+ const testOutputDir = 'test_output';
+
+ beforeEach(() => {
+ // Initialize project directories for testing
+ initProjectDirs('cryptograss.live');
+
+ // Clean up test output directory
+ if (fs.existsSync(testOutputDir)) {
+ fs.rmSync(testOutputDir, { recursive: true });
+ }
+ fs.mkdirSync(testOutputDir, { recursive: true });
+
+ // Set environment for test output
+ process.env.OUTPUT_PRIMARY_ROOT_DIR = testOutputDir;
+ });
+
+ const createMockShow = (showId, venue, tokenStart, tokenCount) => ({
+ [showId]: {
+ title: `Test Show at ${venue}`,
+ venue: venue,
+ locality: 'Test City',
+ region1: 'Test State',
+ local_date: '2025-01-15',
+ blockheight: 22700000 + parseInt(showId.split('-')[1]),
+ poster: 'test-poster.png',
+ has_set_stones_available: false, // Focus on ticket stubs
+ sets: {},
+ ticketStubs: [],
+ ticketStubCount: tokenCount
+ }
+ });
+
+ test('generates claim pages for multiple shows with different token ranges', () => {
+ // Use show IDs that trigger the fake ticket stub generation
+ const mockShows = {
+ ...createMockShow('0_7-22575700', 'Test Venue Alpha', 0, 50), // Burza #4
+ ...createMockShow('0_7-22590100', 'Test Venue Beta', 50, 50), // Bike Jesus
+ ...createMockShow('0-22748946', 'Test Venue Gamma', 100, 40) // Porcupine
+ };
+
+ // This should trigger ticket stub generation and claim page creation
+ expect(() => {
+ generateSetStonePages(mockShows, testOutputDir);
+ }).not.toThrow();
+
+ // Verify claim pages were created for each token ID range
+
+ // Burza #4: tokens 0-49
+ for (let i = 0; i < 50; i++) {
+ const claimPagePath = path.join(testOutputDir, 'cryptograss.live', 'blox-office', 'ticketstubs', 'claim', `${i}.html`);
+ expect(fs.existsSync(claimPagePath)).toBe(true);
+ }
+
+ // Bike Jesus: tokens 50-99
+ for (let i = 50; i < 100; i++) {
+ const claimPagePath = path.join(testOutputDir, 'cryptograss.live', 'blox-office', 'ticketstubs', 'claim', `${i}.html`);
+ expect(fs.existsSync(claimPagePath)).toBe(true);
+ }
+
+ // Porcupine: tokens 100-139
+ for (let i = 100; i < 140; i++) {
+ const claimPagePath = path.join(testOutputDir, 'cryptograss.live', 'blox-office', 'ticketstubs', 'claim', `${i}.html`);
+ expect(fs.existsSync(claimPagePath)).toBe(true);
+ }
+ });
+
+ test('claim pages contain correct show information', () => {
+ const mockShows = createMockShow('0-22748946', 'Test Venue', 100, 5);
+
+ generateSetStonePages(mockShows, testOutputDir);
+
+ // Read a generated claim page (Porcupine range: 100-139)
+ const claimPagePath = path.join(testOutputDir, 'cryptograss.live', 'blox-office', 'ticketstubs', 'claim', '102.html');
+ expect(fs.existsSync(claimPagePath)).toBe(true);
+
+ const claimPageContent = fs.readFileSync(claimPagePath, 'utf8');
+
+ // Verify show details are in the page
+ expect(claimPageContent).toContain('Test Show at Test Venue');
+ expect(claimPageContent).toContain('Test City, Test State');
+ expect(claimPageContent).toContain('2025-01-15');
+ expect(claimPageContent).toContain('Claim Ticket Stub #102');
+ });
+
+ test('claim pages include contract integration code', () => {
+ const mockShows = createMockShow('0-22748946', 'Test Venue', 100, 3);
+
+ generateSetStonePages(mockShows, testOutputDir);
+
+ const claimPagePath = path.join(testOutputDir, 'cryptograss.live', 'blox-office', 'ticketstubs', 'claim', '101.html');
+ const claimPageContent = fs.readFileSync(claimPagePath, 'utf8');
+
+ // Verify blockchain integration elements
+ expect(claimPageContent).toContain('claimTicketStub');
+ expect(claimPageContent).toContain('walletAddress');
+ expect(claimPageContent).toContain('secretInput');
+ expect(claimPageContent).toContain('writeContract');
+ });
+
+ test('does not generate claim pages for shows without ticket stubs', () => {
+ const mockShows = {
+ '0-22700000': {
+ title: 'Show Without Stubs',
+ venue: 'Test Venue',
+ has_set_stones_available: false,
+ sets: {},
+ ticketStubs: [],
+ ticketStubCount: 0
+ }
+ };
+
+ generateSetStonePages(mockShows, testOutputDir);
+
+ // Should not create any claim pages
+ const claimDir = path.join(testOutputDir, 'cryptograss.live', 'blox-office', 'ticketstubs', 'claim');
+ if (fs.existsSync(claimDir)) {
+ const claimFiles = fs.readdirSync(claimDir);
+ expect(claimFiles.length).toBe(0);
+ }
+ });
+
+ test('each token ID gets unique claim page with correct context', () => {
+ const mockShows = createMockShow('0-22748946', 'Context Test Venue', 100, 3);
+
+ generateSetStonePages(mockShows, testOutputDir);
+
+ // Check each generated claim page has the right token ID (Porcupine range: 100-139)
+ for (let tokenId = 100; tokenId < 103; tokenId++) {
+ const claimPagePath = path.join(testOutputDir, 'cryptograss.live', 'blox-office', 'ticketstubs', 'claim', `${tokenId}.html`);
+ const content = fs.readFileSync(claimPagePath, 'utf8');
+
+ expect(content).toContain(`Claim Ticket Stub #${tokenId}`);
+ expect(content).toContain(`tokenId: ${tokenId}`);
+ }
+ });
+});
\ No newline at end of file
diff --git a/__tests__/oracle_of_bluegrass_bacon.test.js b/__tests__/oracle_of_bluegrass_bacon.test.js
new file mode 100644
index 00000000..33dd00c6
--- /dev/null
+++ b/__tests__/oracle_of_bluegrass_bacon.test.js
@@ -0,0 +1,291 @@
+/**
+ * @jest-environment node
+ */
+
+import { BluegrassOracle } from '../src/build_logic/oracle_of_bluegrass_bacon.js';
+
+describe('Oracle of Bluegrass Bacon', () => {
+ let oracle;
+
+ beforeAll(async () => {
+ oracle = new BluegrassOracle();
+ await oracle.buildGraph();
+ });
+
+ describe('Graph Building', () => {
+ test('discovers expected number of musicians', () => {
+ const musicians = oracle.getAllMusicians();
+ expect(musicians.length).toBeGreaterThanOrEqual(50);
+ expect(musicians.length).toBeLessThan(100); // sanity check
+ });
+
+ test('discovers expected number of connections', () => {
+ expect(oracle.connections.length).toBeGreaterThanOrEqual(800);
+ expect(oracle.connections.length).toBeLessThan(1000); // sanity check
+ });
+
+ test('includes core cryptograss musicians', () => {
+ const musicians = oracle.getAllMusicians();
+ expect(musicians).toContain('Justin Holmes');
+ expect(musicians).toContain('Skyler Golden');
+ expect(musicians).toContain('Jakub Vysoky');
+ expect(musicians).toContain('David Grier');
+ expect(musicians).toContain('Cory Walker');
+ });
+
+ test('builds adjacency graph correctly', () => {
+ expect(oracle.graph.has('Justin Holmes')).toBe(true);
+ expect(oracle.graph.get('Justin Holmes').size).toBeGreaterThan(0);
+ });
+
+ test('creates bidirectional connections', () => {
+ // If A connects to B, then B should connect to A
+ const justinConnections = oracle.graph.get('Justin Holmes');
+ if (justinConnections.has('David Grier')) {
+ const grierConnections = oracle.graph.get('David Grier');
+ expect(grierConnections.has('Justin Holmes')).toBe(true);
+ }
+ });
+ });
+
+ describe('Connection Types', () => {
+ test('creates show connections', () => {
+ const showConnections = oracle.connections.filter(c => c.type === 'show');
+ expect(showConnections.length).toBeGreaterThan(0);
+ });
+
+ test('creates recording connections', () => {
+ const recordingConnections = oracle.connections.filter(c => c.type === 'recording');
+ expect(recordingConnections.length).toBeGreaterThan(0);
+ });
+
+ test('show connections have venue information', () => {
+ const showConnection = oracle.connections.find(c =>
+ c.type === 'show' && c.venue && c.context
+ );
+ expect(showConnection).toBeDefined();
+ expect(showConnection.venue).toBeDefined();
+ expect(showConnection.context).toBeDefined();
+ });
+
+ test('recording connections have album information', () => {
+ const recordingConnection = oracle.connections.find(c =>
+ c.type === 'recording' && c.context
+ );
+ expect(recordingConnection).toBeDefined();
+ expect(recordingConnection.context).toBeDefined();
+ });
+ });
+
+ describe('Pathfinding Algorithm', () => {
+ test('finds direct connection (1 degree)', () => {
+ // Justin Holmes and David Grier have collaborated directly
+ const result = oracle.findPath('Justin Holmes', 'David Grier');
+
+ expect(result).not.toBeNull();
+ expect(result.degrees).toBe(1);
+ expect(result.path).toEqual(['Justin Holmes', 'David Grier']);
+ expect(result.connections).toHaveLength(1);
+ });
+
+ test('finds 2-degree connection', () => {
+ // Find a known 2-degree path
+ const result = oracle.findPath('Bones', 'Ice Quilitz');
+
+ if (result) {
+ expect(result.degrees).toBe(2);
+ expect(result.path).toHaveLength(3);
+ expect(result.connections).toHaveLength(2);
+ expect(result.path[0]).toBe('Bones');
+ expect(result.path[2]).toBe('Ice Quilitz');
+ // Middle connection should be through Justin Holmes
+ expect(result.path[1]).toBe('Justin Holmes');
+ }
+ });
+
+ test('handles same musician input', () => {
+ const result = oracle.findPath('Justin Holmes', 'Justin Holmes');
+
+ expect(result.degrees).toBe(0);
+ expect(result.path).toEqual(['Justin Holmes']);
+ expect(result.connections).toHaveLength(0);
+ });
+
+ test('throws error for non-existent musician', () => {
+ expect(() => {
+ oracle.findPath('Non Existent Musician', 'Justin Holmes');
+ }).toThrow('not found in database');
+ });
+
+ test('returns null for disconnected musicians', () => {
+ // This test assumes we might have disconnected components
+ // If all musicians are connected, this test might need adjustment
+ const result = oracle.findPath('Justin Holmes', 'Justin Holmes');
+ expect(result).not.toBeNull(); // This should always work
+ });
+ });
+
+ describe('Path Reconstruction', () => {
+ test('reconstructs connection details correctly', () => {
+ const result = oracle.findPath('Justin Holmes', 'Skyler Golden');
+
+ expect(result).not.toBeNull();
+ expect(result.connections.length).toBe(result.degrees);
+
+ // Each connection should link consecutive musicians in the path
+ for (let i = 0; i < result.connections.length; i++) {
+ const connection = result.connections[i];
+ const musician1 = result.path[i];
+ const musician2 = result.path[i + 1];
+
+ expect(
+ (connection.musicians[0] === musician1 && connection.musicians[1] === musician2) ||
+ (connection.musicians[0] === musician2 && connection.musicians[1] === musician1)
+ ).toBe(true);
+ }
+ });
+
+ test('includes connection metadata', () => {
+ const result = oracle.findPath('Justin Holmes', 'David Grier');
+
+ expect(result).not.toBeNull();
+ expect(result.connections.length).toBeGreaterThan(0);
+
+ const connection = result.connections[0];
+ expect(connection.type).toBeDefined();
+ expect(connection.context).toBeDefined();
+ expect(['show', 'recording']).toContain(connection.type);
+ });
+ });
+
+ describe('Performance and Edge Cases', () => {
+ test('handles all musician combinations efficiently', () => {
+ const musicians = oracle.getAllMusicians().slice(0, 10); // Test subset for performance
+ const startTime = Date.now();
+
+ let pathsFound = 0;
+ let totalDegrees = 0;
+
+ for (let i = 0; i < musicians.length; i++) {
+ for (let j = i + 1; j < musicians.length; j++) {
+ const result = oracle.findPath(musicians[i], musicians[j]);
+ if (result) {
+ pathsFound++;
+ totalDegrees += result.degrees;
+ }
+ }
+ }
+
+ const endTime = Date.now();
+ const duration = endTime - startTime;
+
+ expect(duration).toBeLessThan(5000); // Should complete in under 5 seconds
+ expect(pathsFound).toBeGreaterThan(0);
+
+ if (pathsFound > 0) {
+ const averageDegrees = totalDegrees / pathsFound;
+ expect(averageDegrees).toBeLessThan(6); // Six degrees or less!
+ }
+ });
+
+ test('validates connection consistency', () => {
+ // Every connection should have exactly 2 musicians
+ oracle.connections.forEach(connection => {
+ expect(connection.musicians).toHaveLength(2);
+ expect(connection.musicians[0]).not.toBe(connection.musicians[1]);
+
+ // Both musicians should exist in the musicians map
+ expect(oracle.musicians.has(connection.musicians[0])).toBe(true);
+ expect(oracle.musicians.has(connection.musicians[1])).toBe(true);
+ });
+ });
+
+ test('validates musician data integrity', () => {
+ oracle.musicians.forEach((musicianData, name) => {
+ expect(musicianData.name).toBe(name);
+ expect(musicianData.id).toBeDefined();
+ expect(musicianData.connections).toBeDefined();
+ expect(musicianData.connections.size).toBeGreaterThanOrEqual(0);
+ });
+ });
+ });
+
+ describe('Data Source Integration', () => {
+ test('parses show data correctly', () => {
+ const showConnections = oracle.connections.filter(c => c.type === 'show');
+ expect(showConnections.length).toBeGreaterThan(0);
+
+ // Check for expected show venues/contexts
+ const contexts = showConnections.map(c => c.context);
+ expect(contexts.some(context =>
+ context && context.toLowerCase().includes('porcupine')
+ )).toBe(true); // Porcupine 2025 is in the data
+ });
+
+ test('parses studio recording data correctly', () => {
+ const recordingConnections = oracle.connections.filter(c => c.type === 'recording');
+ expect(recordingConnections.length).toBeGreaterThan(0);
+
+ // Should find connections from known albums
+ const contexts = recordingConnections.map(c => c.context);
+ expect(contexts).toContain('Vowel Sounds');
+ });
+
+ test('handles ensemble modifications correctly', () => {
+ // Look for connections that should include featuring musicians
+ const featuringConnections = oracle.connections.filter(c =>
+ c.songContext && typeof c.songContext === 'object'
+ );
+
+ // This depends on the actual data structure
+ expect(featuringConnections.length).toBeGreaterThanOrEqual(0);
+ });
+ });
+
+ describe('Real-world Test Cases', () => {
+ test('finds expected path: Pepa Lopera to Jakub Vysoky', () => {
+ const result = oracle.findPath('Pepa Lopera', 'Jakub Vysoky');
+
+ if (result) {
+ expect(result.degrees).toBeLessThanOrEqual(2); // Should be direct or through Justin
+ expect(result.path[0]).toBe('Pepa Lopera');
+ expect(result.path[result.path.length - 1]).toBe('Jakub Vysoky');
+ }
+ });
+
+ test('verifies Justin Holmes is highly connected', () => {
+ const justinConnections = oracle.graph.get('Justin Holmes');
+ expect(justinConnections.size).toBeGreaterThan(10); // Should be very connected
+
+ // Justin should be able to reach most musicians in 2 degrees or less
+ const musicians = oracle.getAllMusicians();
+ let reachableInTwoDegrees = 0;
+
+ for (const musician of musicians.slice(0, 20)) { // Test subset
+ if (musician === 'Justin Holmes') continue;
+
+ const result = oracle.findPath('Justin Holmes', musician);
+ if (result && result.degrees <= 2) {
+ reachableInTwoDegrees++;
+ }
+ }
+
+ expect(reachableInTwoDegrees).toBeGreaterThan(15); // Most should be reachable
+ });
+ });
+});
+
+describe('Oracle Data Generation', () => {
+ test('generates consistent static data', async () => {
+ // This would test the build_oracle_data.js output
+ // For now, just verify the oracle can be built consistently
+ const oracle1 = new BluegrassOracle();
+ await oracle1.buildGraph();
+
+ const oracle2 = new BluegrassOracle();
+ await oracle2.buildGraph();
+
+ expect(oracle1.musicians.size).toBe(oracle2.musicians.size);
+ expect(oracle1.connections.length).toBe(oracle2.connections.length);
+ });
+});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 558f1d60..efa0139f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,6 +40,7 @@
"tippy.js": "^6.3.7",
"viem": "^2.9.28",
"web3": "^4.10.0",
+ "webamp": "^2.2.0",
"webpack-merge": "^5.10.0"
},
"devDependencies": {
@@ -77,6 +78,12 @@
"node": ">=6.0.0"
}
},
+ "node_modules/@assemblyscript/loader": {
+ "version": "0.17.14",
+ "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.17.14.tgz",
+ "integrity": "sha512-+PVTOfla/0XMLRTQLJFPg4u40XcdTfon6GGea70hBGi8Pd7ZymIXyVUR+vK8wt5Jb4MVKTKPIz43Myyebw5mZA==",
+ "license": "Apache-2.0"
+ },
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@@ -1817,6 +1824,16 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"license": "MIT"
},
+ "node_modules/@borewit/text-codec": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.0.tgz",
+ "integrity": "sha512-X999CKBxGwX8wW+4gFibsbiNdwqmdQEXmUejIWaIqdrHBgS5ARIOOeyiQbHjP9G58xVEPcuvP6VwwH3A0OFTOA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/@coinbase/wallet-sdk": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.2.3.tgz",
@@ -3857,6 +3874,19 @@
"url": "https://opencollective.com/popperjs"
}
},
+ "node_modules/@redux-devtools/extension": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz",
+ "integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "immutable": "^4.3.4"
+ },
+ "peerDependencies": {
+ "redux": "^3.1.0 || ^4.0.0 || ^5.0.0"
+ }
+ },
"node_modules/@safe-global/safe-apps-provider": {
"version": "0.18.4",
"resolved": "https://registry.npmjs.org/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.4.tgz",
@@ -3988,6 +4018,117 @@
"url": "https://paulmillr.com/funding/"
}
},
+ "node_modules/@sentry/browser": {
+ "version": "5.9.1",
+ "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-5.9.1.tgz",
+ "integrity": "sha512-7AOabwp9yAH9h6Xe6TfDwlLxHbUSWs+SPWHI7bPlht2yDSAqkXYGSzRr5X0XQJX9oBQdx2cEPMqHyJrbNaP/og==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sentry/core": "5.8.0",
+ "@sentry/types": "5.7.1",
+ "@sentry/utils": "5.8.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/browser/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
+ "node_modules/@sentry/core": {
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-5.8.0.tgz",
+ "integrity": "sha512-aAh2KLidIXJVGrxmHSVq2eVKbu7tZiYn5ylW6yzJXFetS5z4MA+JYaSBaG2inVYDEEqqMIkb17TyWxxziUDieg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sentry/hub": "5.8.0",
+ "@sentry/minimal": "5.8.0",
+ "@sentry/types": "5.7.1",
+ "@sentry/utils": "5.8.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/core/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
+ "node_modules/@sentry/hub": {
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/@sentry/hub/-/hub-5.8.0.tgz",
+ "integrity": "sha512-VdApn1ZCNwH1wwQwoO6pu53PM/qgHG+DQege0hbByluImpLBhAj9w50nXnF/8KzV4UoMIVbzCb6jXzMRmqqp9A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sentry/types": "5.7.1",
+ "@sentry/utils": "5.8.0",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/hub/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
+ "node_modules/@sentry/minimal": {
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/@sentry/minimal/-/minimal-5.8.0.tgz",
+ "integrity": "sha512-MIlFOgd+JvAUrBBmq7vr9ovRH1HvckhnwzHdoUPpKRBN+rQgTyZy1o6+kA2fASCbrRqFCP+Zk7EHMACKg8DpIw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sentry/hub": "5.8.0",
+ "@sentry/types": "5.7.1",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/minimal/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
+ "node_modules/@sentry/types": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/@sentry/types/-/types-5.7.1.tgz",
+ "integrity": "sha512-tbUnTYlSliXvnou5D4C8Zr+7/wJrHLbpYX1YkLXuIJRU0NSi81bHMroAuHWILcQKWhVjaV/HZzr7Y/hhWtbXVQ==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/utils": {
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-5.8.0.tgz",
+ "integrity": "sha512-KDxUvBSYi0/dHMdunbxAxD3389pcQioLtcO6CI6zt/nJXeVFolix66cRraeQvqupdLhvOk/el649W4fCPayTHw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@sentry/types": "5.7.1",
+ "tslib": "^1.9.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/@sentry/utils/node_modules/tslib": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
+ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
+ "license": "0BSD"
+ },
"node_modules/@sinclair/typebox": {
"version": "0.27.8",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz",
@@ -4195,6 +4336,47 @@
"@stablelib/wipe": "^1.0.1"
}
},
+ "node_modules/@tokenizer/inflate": {
+ "version": "0.2.7",
+ "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
+ "integrity": "sha512-MADQgmZT1eKjp06jpI2yozxaU9uVs4GzzgSL+uEq7bVcJ9V1ZXQkeGNql1fsSI0gMy1vhvNTNbUqrx+pZfJVmg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.0",
+ "fflate": "^0.8.2",
+ "token-types": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/@tokenizer/inflate/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tokenizer/token": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
+ "license": "MIT"
+ },
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -4375,6 +4557,18 @@
"@types/node": "*"
}
},
+ "node_modules/@types/hoist-non-react-statics": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.7.tgz",
+ "integrity": "sha512-PQTyIulDkIDro8P+IHbKCsw7U2xxBYflVzW/FgWdCAePD9xGSidgA76/GeJ6lBKoblyhf9pBY763gbrN+1dI8g==",
+ "license": "MIT",
+ "dependencies": {
+ "hoist-non-react-statics": "^3.3.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@@ -4469,6 +4663,13 @@
"@types/node": "*"
}
},
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/@types/qs": {
"version": "6.9.17",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz",
@@ -4483,6 +4684,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/react": {
+ "version": "18.3.24",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
+ "integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.0.2"
+ }
+ },
"node_modules/@types/retry": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz",
@@ -4545,6 +4757,12 @@
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT"
},
+ "node_modules/@types/use-sync-external-store": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
+ "integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
+ "license": "MIT"
+ },
"node_modules/@types/ws": {
"version": "8.5.13",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
@@ -6223,6 +6441,16 @@
"ajv": "^8.8.2"
}
},
+ "node_modules/ani-cursor": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/ani-cursor/-/ani-cursor-0.0.5.tgz",
+ "integrity": "sha512-gGxst72lG9TOwEfbVpX9vHhzUGw+4Ee2XB6AfYq5JP+bxBtpAjgnTBepCVxYF5t1TPrWHN23nWqLTflJOA3/ag==",
+ "license": "MIT",
+ "dependencies": {
+ "byte-data": "18.1.1",
+ "riff-file": "^1.0.3"
+ }
+ },
"node_modules/ansi-escapes": {
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz",
@@ -6333,6 +6561,31 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
+ "node_modules/assert": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz",
+ "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==",
+ "license": "MIT",
+ "dependencies": {
+ "object.assign": "^4.1.4",
+ "util": "^0.10.4"
+ }
+ },
+ "node_modules/assert/node_modules/inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==",
+ "license": "ISC"
+ },
+ "node_modules/assert/node_modules/util": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "2.0.3"
+ }
+ },
"node_modules/async-mutex": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.2.6.tgz",
@@ -6546,6 +6799,22 @@
"@babel/core": "^7.0.0"
}
},
+ "node_modules/babel-runtime": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
+ "integrity": "sha512-ITKNuq2wKlW1fJg9sSW52eepoYgZBggvOAHC0u/CYu/qxQ9EVzThCgR69BnSXLHjy2f7SY5zaQ4yt7H9ZVxY2g==",
+ "license": "MIT",
+ "dependencies": {
+ "core-js": "^2.4.0",
+ "regenerator-runtime": "^0.11.0"
+ }
+ },
+ "node_modules/babel-runtime/node_modules/regenerator-runtime": {
+ "version": "0.11.1",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
+ "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
+ "license": "MIT"
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -6838,6 +7107,40 @@
"node": ">=6.14.2"
}
},
+ "node_modules/butterchurn": {
+ "version": "3.0.0-beta.5",
+ "resolved": "https://registry.npmjs.org/butterchurn/-/butterchurn-3.0.0-beta.5.tgz",
+ "integrity": "sha512-BStK4OAbBb9Pvt8PuQlS4WVmYBwU1KuDMRHF1V89QjoIFauAqq7tpV4EpYXj7K563r5daLrMX+2y5DBhZZ9Xig==",
+ "license": "MIT",
+ "dependencies": {
+ "@assemblyscript/loader": "^0.17.11",
+ "ecma-proposal-math-extensions": "0.0.2",
+ "eel-wasm": "^0.0.16"
+ }
+ },
+ "node_modules/butterchurn-presets": {
+ "version": "3.0.0-beta.4",
+ "resolved": "https://registry.npmjs.org/butterchurn-presets/-/butterchurn-presets-3.0.0-beta.4.tgz",
+ "integrity": "sha512-TbQLUPvGOYMZAtWKoCmBtludh9aQZ6NaMGQU4lvPeadBPy3Du3yNmwBjlTMLP5c5mRWElxQPjTL1PtR7FZK3OQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ }
+ },
+ "node_modules/byte-data": {
+ "version": "18.1.1",
+ "resolved": "https://registry.npmjs.org/byte-data/-/byte-data-18.1.1.tgz",
+ "integrity": "sha512-Kv/B0r7adgnCcrs/y703sac2XFLdHW5kPfis1j8+Ij/hmEcWhBKf+1pNTv+vsNqXb207Uiyri8bpnogNxR/4Lg==",
+ "license": "MIT",
+ "dependencies": {
+ "endianness": "^8.0.2",
+ "ieee754-buffer": "^2.0.0",
+ "utf8-buffer": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -6849,16 +7152,44 @@
}
},
"node_modules/call-bind": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
- "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
"license": "MIT",
"dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
"es-define-property": "^1.0.0",
- "es-errors": "^1.3.0",
- "function-bind": "^1.1.2",
"get-intrinsic": "^1.2.4",
- "set-function-length": "^1.2.1"
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
@@ -6954,6 +7285,18 @@
"sha.js": "^2.4.11"
}
},
+ "node_modules/chainsaw": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.0.9.tgz",
+ "integrity": "sha512-nG8PYH+/4xB+8zkV4G844EtfvZ5tTiLFoX3dZ4nhF4t3OCKIb9UvaFyNmeZO2zOSmRWzBoTD+napN6hiL+EgcA==",
+ "license": "MIT/X11",
+ "dependencies": {
+ "traverse": ">=0.3.0 <0.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -7073,6 +7416,12 @@
"integrity": "sha512-cuSVIHi9/9E/+821Qjdvngor+xpnlwnuwIyZOaLmHBVdXL+gP+I6QQB9VkO7RI77YIcTV+S1W9AreJ5eN63JBA==",
"license": "MIT"
},
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
+ },
"node_modules/clean-css": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
@@ -7638,7 +7987,6 @@
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -7698,6 +8046,14 @@
"webpack": "^5.1.0"
}
},
+ "node_modules/core-js": {
+ "version": "2.6.12",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz",
+ "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==",
+ "deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
+ "hasInstallScript": true,
+ "license": "MIT"
+ },
"node_modules/core-js-compat": {
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
@@ -7868,6 +8224,13 @@
"node": ">=4"
}
},
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT",
+ "peer": true
+ },
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
@@ -8012,6 +8375,23 @@
"node": ">=8"
}
},
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/defu": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz",
@@ -8334,6 +8714,20 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/duplexify": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz",
@@ -8369,6 +8763,12 @@
"node": ">=16"
}
},
+ "node_modules/ecma-proposal-math-extensions": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/ecma-proposal-math-extensions/-/ecma-proposal-math-extensions-0.0.2.tgz",
+ "integrity": "sha512-80BnDp2Fn7RxXlEr5HHZblniY4aQ97MOAicdWWpSo0vkQiISSE9wLR4SqxKsu4gCtXFBIPPzy8JMhay4NWRg/Q==",
+ "license": "MIT"
+ },
"node_modules/ee-first": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@@ -8376,6 +8776,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/eel-wasm": {
+ "version": "0.0.16",
+ "resolved": "https://registry.npmjs.org/eel-wasm/-/eel-wasm-0.0.16.tgz",
+ "integrity": "sha512-1tkId7I7E1Vs4fXTRsH83Sjn2S/AbzrVQKLBRGys6NLc3eVH4NBffJsdEeLHOWWUgQpVXBEP3CV/srUZNIuBnw==",
+ "license": "MIT"
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.70",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.70.tgz",
@@ -8446,6 +8852,15 @@
"once": "^1.4.0"
}
},
+ "node_modules/endianness": {
+ "version": "8.0.2",
+ "resolved": "https://registry.npmjs.org/endianness/-/endianness-8.0.2.tgz",
+ "integrity": "sha512-IU+77+jJ7lpw2qZ3NUuqBZFy3GuioNgXUdsL1L9tooDNTaw0TgOnwNuc+8Ns+haDaTifK97QLzmOANJtI/rGvw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/engine.io-client": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz",
@@ -8536,13 +8951,10 @@
}
},
"node_modules/es-define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
- "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
- "dependencies": {
- "get-intrinsic": "^1.2.4"
- },
"engines": {
"node": ">= 0.4"
}
@@ -8563,6 +8975,18 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@@ -9189,17 +9613,41 @@
"urlsafe-base64": "1.0.0"
}
},
- "node_modules/fill-range": {
- "version": "7.1.1",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
- "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "node_modules/fflate": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
+ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
+ "license": "MIT"
+ },
+ "node_modules/file-type": {
+ "version": "21.0.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.0.0.tgz",
+ "integrity": "sha512-ek5xNX2YBYlXhiUXui3D/BXa3LdqPmoLJ7rqEx2bKJ7EAUEfmXgW0Das7Dc6Nr9MvqaOnIqiPV0mZk/r/UpNAg==",
"license": "MIT",
"dependencies": {
- "to-regex-range": "^5.0.1"
+ "@tokenizer/inflate": "^0.2.7",
+ "strtok3": "^10.2.2",
+ "token-types": "^6.0.0",
+ "uint8array-extras": "^1.4.0"
},
"engines": {
- "node": ">=8"
- }
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+ "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+ "license": "MIT",
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
},
"node_modules/filter-obj": {
"version": "1.1.0",
@@ -9353,6 +9801,12 @@
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
"license": "ISC"
},
+ "node_modules/fscreen": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fscreen/-/fscreen-1.2.0.tgz",
+ "integrity": "sha512-hlq4+BU0hlPmwsFjwGGzZ+OZ9N/wq9Ljg/sq3pX+2CD7hrJsX9tJgWWK/wiNTFM212CLHWhicOoqwXyZGGetJg==",
+ "license": "MIT"
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -9407,16 +9861,21 @@
}
},
"node_modules/get-intrinsic": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
- "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
- "has-proto": "^1.0.1",
- "has-symbols": "^1.0.3",
- "hasown": "^2.0.0"
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
@@ -9440,6 +9899,19 @@
"integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==",
"license": "MIT"
},
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/get-stream": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
@@ -9528,6 +10000,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/glsl-optimizer-js": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/glsl-optimizer-js/-/glsl-optimizer-js-0.0.2.tgz",
+ "integrity": "sha512-SMkVILyc1LeBEBgiHOe+4Bh8MEqxLNyAns0NfgmxJTxZZdj7oCoZt+n846rbdB8OLGsg16f5C9nmhi9XEuM8SQ==",
+ "license": "MIT"
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -9592,21 +10070,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-proto": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.1.0.tgz",
- "integrity": "sha512-QLdzI9IIO1Jg7f9GT1gXpPpXArAn6cS31R1eEZqz08Gc+uQ8/XiqHWt17Fiw+2p6oTTIq5GXEpQkAlA88YRl/Q==",
- "license": "MIT",
- "dependencies": {
- "call-bind": "^1.0.7"
- },
- "engines": {
- "node": ">= 0.4"
- },
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
@@ -9644,6 +10107,18 @@
"minimalistic-assert": "^1.0.1"
}
},
+ "node_modules/hashish": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/hashish/-/hashish-0.0.4.tgz",
+ "integrity": "sha512-xyD4XgslstNAs72ENaoFvgMwtv8xhiDtC2AtzCG+8yF7W/Knxxm9BX+e2s25mm+HxMKh0rBmXVOEGF3zNImXvA==",
+ "license": "MIT/X11",
+ "dependencies": {
+ "traverse": ">=0.2.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -9683,6 +10158,21 @@
"minimalistic-crypto-utils": "^1.0.1"
}
},
+ "node_modules/hoist-non-react-statics": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
+ "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "react-is": "^16.7.0"
+ }
+ },
+ "node_modules/hoist-non-react-statics/node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
+ "license": "MIT"
+ },
"node_modules/howler": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/howler/-/howler-2.2.4.tgz",
@@ -9982,6 +10472,15 @@
],
"license": "BSD-3-Clause"
},
+ "node_modules/ieee754-buffer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ieee754-buffer/-/ieee754-buffer-2.0.0.tgz",
+ "integrity": "sha512-AXUAT0nMEi7h1Is8HXGXof3eejl/GabZFKSj8Ym6kVRUSwrAb52EkAXywiCQYSHGQMRn7lvfY7vhPMjVc+Kybg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -9992,6 +10491,18 @@
"node": ">= 4"
}
},
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
+ "node_modules/immutable": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
+ "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
+ "license": "MIT"
+ },
"node_modules/import-local": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
@@ -10053,6 +10564,15 @@
"node": ">=10.13.0"
}
},
+ "node_modules/invariant": {
+ "version": "2.2.4",
+ "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz",
+ "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.0.0"
+ }
+ },
"node_modules/ipaddr.js": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
@@ -10329,6 +10849,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
+ "license": "MIT"
+ },
"node_modules/is-unicode-supported": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
@@ -11350,6 +11876,48 @@
"node": ">=6"
}
},
+ "node_modules/jszip": {
+ "version": "3.10.1",
+ "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
+ "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
+ "license": "(MIT OR GPL-3.0-or-later)",
+ "dependencies": {
+ "lie": "~3.3.0",
+ "pako": "~1.0.2",
+ "readable-stream": "~2.3.6",
+ "setimmediate": "^1.0.5"
+ }
+ },
+ "node_modules/jszip/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/jszip/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/jszip/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
"node_modules/keccak": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/keccak/-/keccak-3.0.4.tgz",
@@ -11409,6 +11977,15 @@
"node": ">=6"
}
},
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
@@ -11568,7 +12145,6 @@
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
@@ -11631,11 +12207,19 @@
"node": ">= 18"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -11715,6 +12299,34 @@
"node": ">=8.6"
}
},
+ "node_modules/milkdrop-eel-parser": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/milkdrop-eel-parser/-/milkdrop-eel-parser-0.0.4.tgz",
+ "integrity": "sha512-4PsOdTMDB7GM3UFzqXQQXf8MBeoolOhsBLMlhug+IIMZ+yNkvqLbdqDbrueGZc8P8tLRJP8pbAxna1yjFr06HQ==",
+ "license": "MIT"
+ },
+ "node_modules/milkdrop-preset-converter-aws": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/milkdrop-preset-converter-aws/-/milkdrop-preset-converter-aws-0.1.6.tgz",
+ "integrity": "sha512-nr89LRZYgdrDn17vGQCvUK/LM9d90mywElL7zlzXBTgkxWAs/Kamn1Yl9676ugt4L4BAGo6PTEipIqeYXFSM7g==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-runtime": "^6.26.0",
+ "glsl-optimizer-js": "^0.0.2",
+ "milkdrop-eel-parser": "^0.0.4",
+ "milkdrop-preset-utils": "^0.1.0"
+ }
+ },
+ "node_modules/milkdrop-preset-utils": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/milkdrop-preset-utils/-/milkdrop-preset-utils-0.1.0.tgz",
+ "integrity": "sha512-yK5y03SN8INC+ssLLYGGsaAHgNxXEUK6PQVV44rg9OAA27F2aPM0tA5uGsDdASH9sgPaAaRVMV5NoEvEkh66Sw==",
+ "license": "MIT",
+ "dependencies": {
+ "babel-runtime": "^6.26.0",
+ "lodash": "^4.17.4"
+ }
+ },
"node_modules/mime": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz",
@@ -11927,6 +12539,153 @@
"integrity": "sha512-HoMUjhH9T8DDBNT+6xzkrd9ga/XiBI4xLr58LJACwK6G3HTOPeMz4nB4KJs33L2BelrIJa7P0VuNaVF3hMYfjg==",
"license": "(Apache-2.0 AND MIT)"
},
+ "node_modules/music-metadata": {
+ "version": "11.8.3",
+ "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-11.8.3.tgz",
+ "integrity": "sha512-Tgiv4MlCgDb6XzelziB1mmL2xeoHls0KTpCm3Z3qr+LfF4mBEpkuc5vNrc927IT5+S5fv+vzStfI+HYC0igDpA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ },
+ {
+ "type": "buymeacoffee",
+ "url": "https://buymeacoffee.com/borewit"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@borewit/text-codec": "^0.2.0",
+ "@tokenizer/token": "^0.3.0",
+ "content-type": "^1.0.5",
+ "debug": "^4.4.1",
+ "file-type": "^21.0.0",
+ "media-typer": "^1.1.0",
+ "strtok3": "^10.3.4",
+ "token-types": "^6.1.1",
+ "uint8array-extras": "^1.4.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/music-metadata-browser": {
+ "version": "0.6.6",
+ "resolved": "https://registry.npmjs.org/music-metadata-browser/-/music-metadata-browser-0.6.6.tgz",
+ "integrity": "sha512-14KFz4HR6rM6RATcLtJoBDRbehU/dKdVzElCdeI8CjP7Un2HtSf0WiT7f7Lz+XNkcBMZUjthmC6Wy4+NNayCRw==",
+ "deprecated": "No longer support, superseded by music-metadata",
+ "license": "MIT",
+ "dependencies": {
+ "assert": "^1.4.1",
+ "buffer": "^5.2.1",
+ "debug": "^4.0.1",
+ "music-metadata": "^3.4.0",
+ "readable-stream": "^3.0.6",
+ "remove": "^0.1.5",
+ "typedarray-to-buffer": "^3.1.5"
+ }
+ },
+ "node_modules/music-metadata-browser/node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/music-metadata-browser/node_modules/file-type": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-11.1.0.tgz",
+ "integrity": "sha512-rM0UO7Qm9K7TWTtA6AShI/t7H5BPjDeGVDaNyg9BjHAj3PysKy7+8C8D137R88jnR3rFJZQB/tFgydl5sN5m7g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/music-metadata-browser/node_modules/music-metadata": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/music-metadata/-/music-metadata-3.8.0.tgz",
+ "integrity": "sha512-aIADbp3uCS+ANr4nnFEHzTzMy81OT7PR7WBMW73SJ28Y7P94nnEugmTOj1ICP2JmxBBDlo+MeYVgiPnxVN69tg==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "file-type": "^11.0.0",
+ "media-typer": "0.3.0",
+ "strtok3": "^2.3.0",
+ "token-types": "^1.0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/music-metadata-browser/node_modules/strtok3": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-2.3.0.tgz",
+ "integrity": "sha512-AA67/1atBh7X0fUTDevjW89by2ZkY9RZAnkwusx5Yc1COYf0ruUbpYOOIs03SnRA1CF9K3+BtRXKOEtKhAXVaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.1.0",
+ "then-read-stream": "^1.5.0",
+ "token-types": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=0.1.98"
+ }
+ },
+ "node_modules/music-metadata-browser/node_modules/token-types": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-1.3.2.tgz",
+ "integrity": "sha512-LemYprKRfZPUiwVEMIL8fIP/cvZBpMds1PklsyoQyLZdKk7SQlldNGzw4TTrg2MnWLGSkMM6gUa1EW0h1d72fg==",
+ "license": "MIT",
+ "dependencies": {
+ "ieee754": "^1.1.13"
+ },
+ "engines": {
+ "node": ">=0.1.98"
+ }
+ },
+ "node_modules/music-metadata/node_modules/debug": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+ "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/music-metadata/node_modules/media-typer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@@ -12194,6 +12953,35 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/obuf": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz",
@@ -12500,6 +13288,12 @@
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
"license": "BlueOak-1.0.0"
},
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@@ -13410,6 +14204,31 @@
"node": ">= 10.13.0"
}
},
+ "node_modules/redux": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
+ "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.9.2"
+ }
+ },
+ "node_modules/redux-sentry-middleware": {
+ "version": "0.1.8",
+ "resolved": "https://registry.npmjs.org/redux-sentry-middleware/-/redux-sentry-middleware-0.1.8.tgz",
+ "integrity": "sha512-xubpYH9RgE31tZUESeRW5agwQa19Yd6Gy+4iO09raW/2TITPO5fhJdXpVwJfpGMbIYhEmHFqE2wD5Lnz7YtAeA==",
+ "license": "MIT"
+ },
+ "node_modules/redux-thunk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
+ "integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
+ "license": "MIT",
+ "peerDependencies": {
+ "redux": "^4"
+ }
+ },
"node_modules/regenerate": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz",
@@ -13494,6 +14313,15 @@
"node": ">= 0.10"
}
},
+ "node_modules/remove": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/remove/-/remove-0.1.5.tgz",
+ "integrity": "sha512-AJMA9oWvJzdTjwIGwSQZsjGQiRx73YTmiOWmfCp1fpLa/D4n7jKcpoA+CZiVLJqKcEKUuh1Suq80c5wF+L/qVQ==",
+ "license": "MIT",
+ "dependencies": {
+ "seq": ">= 0.3.5"
+ }
+ },
"node_modules/renderkid": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz",
@@ -13563,6 +14391,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/reselect": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/reselect/-/reselect-3.0.1.tgz",
+ "integrity": "sha512-b/6tFZCmRhtBMa4xGqiiRp9jh9Aqi2A687Lo265cN0/QohJQEBPiQ52f4QB6i0eF3yp3hmLL21LSGBcML2dlxA==",
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -13662,6 +14496,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/riff-file": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/riff-file/-/riff-file-1.0.3.tgz",
+ "integrity": "sha512-Vv8wwGr0BCks7VMI3Lv0houZee4DaHFjjTT0LMhMJKio2YmLncLeIVpK63ydSverngNk8XQPU3fbeP3bWgSIig==",
+ "license": "MIT",
+ "dependencies": {
+ "byte-data": "^18.0.3"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
@@ -13795,6 +14641,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/scheduler": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz",
+ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==",
+ "license": "MIT"
+ },
"node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
@@ -13922,6 +14774,19 @@
"node": ">=4"
}
},
+ "node_modules/seq": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/seq/-/seq-0.3.5.tgz",
+ "integrity": "sha512-sisY2Ln1fj43KBkRtXkesnRHYNdswIkIibvNe/0UKm2GZxjMbqmccpiatoKr/k2qX5VKiLU8xm+tz/74LAho4g==",
+ "license": "MIT/X11",
+ "dependencies": {
+ "chainsaw": ">=0.0.7 <0.1",
+ "hashish": ">=0.0.2 <0.1"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/serialize-javascript": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
@@ -14642,6 +15507,22 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/strtok3": {
+ "version": "10.3.4",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.4.tgz",
+ "integrity": "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/style-loader": {
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
@@ -14955,6 +15836,16 @@
"node": "*"
}
},
+ "node_modules/then-read-stream": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/then-read-stream/-/then-read-stream-1.5.1.tgz",
+ "integrity": "sha512-I+iiemYWhp1ysJQEioqpEICgvHlqHS5WrQGZkboFLs7Jm350Kvq4cN3qRCzHpETUuq5+NsdrdWEg6M0NFxtwtQ==",
+ "deprecated": "Package renamed to peak-readable.",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/thread-stream": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-0.15.2.tgz",
@@ -14971,6 +15862,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/tinyqueue": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-1.2.3.tgz",
+ "integrity": "sha512-Qz9RgWuO9l8lT+Y9xvbzhPT2efIUIFd69N7eF7tJ9lnQl0iLj1M7peK7IoUGZL9DJHw9XftqLreccfxcQgYLxA==",
+ "license": "ISC"
+ },
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmjs.org/tippy.js/-/tippy.js-6.3.7.tgz",
@@ -15008,12 +15905,49 @@
"node": ">=0.6"
}
},
+ "node_modules/token-types": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.1.tgz",
+ "integrity": "sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@borewit/text-codec": "^0.1.0",
+ "@tokenizer/token": "^0.3.0",
+ "ieee754": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/token-types/node_modules/@borewit/text-codec": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.1.1.tgz",
+ "integrity": "sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
+ "node_modules/traverse": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
+ "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==",
+ "license": "MIT/X11",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
@@ -15083,6 +16017,15 @@
"node": ">= 0.6"
}
},
+ "node_modules/typedarray-to-buffer": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
+ "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
+ "license": "MIT",
+ "dependencies": {
+ "is-typedarray": "^1.0.0"
+ }
+ },
"node_modules/typescript": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
@@ -15103,6 +16046,18 @@
"integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==",
"license": "MIT"
},
+ "node_modules/uint8array-extras": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
+ "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/uint8arrays": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-3.1.0.tgz",
@@ -15386,6 +16341,15 @@
"node": ">=6.14.2"
}
},
+ "node_modules/utf8-buffer": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/utf8-buffer/-/utf8-buffer-1.0.0.tgz",
+ "integrity": "sha512-ueuhzvWnp5JU5CiGSY4WdKbiN/PO2AZ/lpeLiz2l38qwdLy/cW40XobgyuIWucNyum0B33bVB0owjFCeGBSLqg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -15978,6 +16942,103 @@
"npm": ">=6.12.0"
}
},
+ "node_modules/webamp": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/webamp/-/webamp-2.2.0.tgz",
+ "integrity": "sha512-XzKr65Z4d+4rxA1J//aPkZRqvPS0aqAxpryNKaWt/EDQ4uCJadxjr966QElagH+iZxWMCDekW5dV/dTx5b+WPQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@redux-devtools/extension": "^3.3.0",
+ "@sentry/browser": "5.9.1",
+ "ani-cursor": "0.0.5",
+ "butterchurn": "3.0.0-beta.5",
+ "butterchurn-presets": "3.0.0-beta.4",
+ "classnames": "^2.2.5",
+ "fscreen": "^1.0.2",
+ "invariant": "^2.2.3",
+ "jszip": "^3.10.1",
+ "lodash": "^4.17.21",
+ "milkdrop-preset-converter-aws": "^0.1.6",
+ "music-metadata": "^11.6.0",
+ "music-metadata-browser": "^0.6.1",
+ "react": "^19.1.0",
+ "react-dom": "^19.1.0",
+ "react-redux": "^8.0.5",
+ "redux": "^5.0.0-alpha.0",
+ "redux-sentry-middleware": "^0.1.3",
+ "redux-thunk": "^2.4.0",
+ "reselect": "^3.0.1",
+ "strtok3": "^10.3.1",
+ "tinyqueue": "^1.2.3",
+ "winamp-eqf": "1.0.0"
+ }
+ },
+ "node_modules/webamp/node_modules/react": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz",
+ "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/webamp/node_modules/react-dom": {
+ "version": "19.1.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz",
+ "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.26.0"
+ },
+ "peerDependencies": {
+ "react": "^19.1.1"
+ }
+ },
+ "node_modules/webamp/node_modules/react-redux": {
+ "version": "8.1.3",
+ "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.3.tgz",
+ "integrity": "sha512-n0ZrutD7DaX/j9VscF+uTALI3oUPa/pO4Z3soOBIjuRn/FzVu6aehhysxZCLi6y7duMf52WNZGMl7CtuK5EnRw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.1",
+ "@types/hoist-non-react-statics": "^3.3.1",
+ "@types/use-sync-external-store": "^0.0.3",
+ "hoist-non-react-statics": "^3.3.2",
+ "react-is": "^18.0.0",
+ "use-sync-external-store": "^1.0.0"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.8 || ^17.0 || ^18.0",
+ "@types/react-dom": "^16.8 || ^17.0 || ^18.0",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0",
+ "react-native": ">=0.59",
+ "redux": "^4 || ^5.0.0-beta.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "redux": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/webamp/node_modules/redux": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
+ "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
+ "license": "MIT"
+ },
"node_modules/webauthn-p256": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/webauthn-p256/-/webauthn-p256-0.0.10.tgz",
@@ -16373,6 +17434,12 @@
"integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==",
"license": "MIT"
},
+ "node_modules/winamp-eqf": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/winamp-eqf/-/winamp-eqf-1.0.0.tgz",
+ "integrity": "sha512-yUIb4+lTYBKP4L6nPXdDj1CQBXlJ+/PrNAkT1VbTAgeFjX8lPxAthsUE5NxQP4s8SO4YMJemsrErZ49Bh+/Veg==",
+ "license": "ISC"
+ },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
diff --git a/package.json b/package.json
index 78a7283e..1731d5c9 100644
--- a/package.json
+++ b/package.json
@@ -13,7 +13,8 @@
"fetch-video-metadata": "node src/build_logic/fetch_video_metadata.js",
"verify-dice": "node src/build_logic/verify_dice_roll.js",
"fetch-chain-data": "node src/build_logic/fetch_chain_data.js",
- "blockheight": "node src/build_logic/get_current_blockheight.js"
+ "blockheight": "node src/build_logic/get_current_blockheight.js",
+ "build-oracle": "node src/build_logic/build_oracle_data.js"
},
"type": "module",
"keywords": [],
@@ -66,6 +67,7 @@
"tippy.js": "^6.3.7",
"viem": "^2.9.28",
"web3": "^4.10.0",
+ "webamp": "^2.2.0",
"webpack-merge": "^5.10.0"
}
}
diff --git a/src/build_logic/build_oracle_data.js b/src/build_logic/build_oracle_data.js
new file mode 100644
index 00000000..23f02d88
--- /dev/null
+++ b/src/build_logic/build_oracle_data.js
@@ -0,0 +1,94 @@
+/**
+ * Build Oracle of Bluegrass Bacon data for static serving
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { fileURLToPath } from 'url';
+import { BluegrassOracle } from './oracle_of_bluegrass_bacon.js';
+import { initProjectDirs } from './locations.js';
+
+// Initialize for cryptograss.live since that's where the Oracle lives
+initProjectDirs("cryptograss.live");
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+async function buildOracleData() {
+ console.log('🎵 Building Oracle of Bluegrass Bacon data...');
+
+ const oracle = new BluegrassOracle();
+ await oracle.buildGraph();
+
+ // Output to the API directory where the Oracle expects to find it
+ const apiDir = path.join(path.dirname(__dirname), 'sites', 'cryptograss.live', 'api', 'oracle');
+ fs.mkdirSync(apiDir, { recursive: true });
+
+ // Generate musicians list
+ const musicians = oracle.getAllMusicians();
+ fs.writeFileSync(
+ path.join(apiDir, 'musicians.json'),
+ JSON.stringify(musicians, null, 2)
+ );
+
+ // Build a lookup object for fast path finding
+ // Since this is for a website, we'll pre-compute some popular paths
+ // and create a simplified graph representation
+ const graphData = {
+ musicians: {},
+ connections: oracle.connections.map(conn => ({
+ id: conn.id,
+ musicians: conn.musicians,
+ type: conn.type,
+ context: conn.context,
+ venue: conn.venue,
+ location: conn.location,
+ blockHeight: conn.blockHeight,
+ songContext: conn.songContext,
+ song: conn.song
+ }))
+ };
+
+ // Add musician data with connections
+ for (const [name, data] of oracle.musicians) {
+ graphData.musicians[name] = {
+ id: data.id,
+ name: data.name,
+ connections: Array.from(data.connections)
+ };
+ }
+
+ // Create adjacency list for client-side pathfinding
+ const adjacencyList = {};
+ for (const musician of musicians) {
+ adjacencyList[musician] = Array.from(oracle.graph.get(musician));
+ }
+
+ const fullData = {
+ musicians: graphData.musicians,
+ connections: graphData.connections,
+ adjacencyList: adjacencyList,
+ stats: {
+ totalMusicians: musicians.length,
+ totalConnections: oracle.connections.length,
+ buildTime: new Date().toISOString()
+ }
+ };
+
+ fs.writeFileSync(
+ path.join(apiDir, 'graph.json'),
+ JSON.stringify(fullData, null, 2)
+ );
+
+ console.log(`✅ Built Oracle data:`);
+ console.log(` - ${musicians.length} musicians`);
+ console.log(` - ${oracle.connections.length} connections`);
+ console.log(` - Output: ${apiDir}`);
+}
+
+// CLI usage
+if (import.meta.url === `file://${process.argv[1]}`) {
+ buildOracleData().catch(console.error);
+}
+
+export { buildOracleData };
\ No newline at end of file
diff --git a/src/build_logic/oracle_of_bluegrass_bacon.js b/src/build_logic/oracle_of_bluegrass_bacon.js
new file mode 100644
index 00000000..06a1d4e1
--- /dev/null
+++ b/src/build_logic/oracle_of_bluegrass_bacon.js
@@ -0,0 +1,319 @@
+/**
+ * Oracle of Bluegrass Bacon - Connect any two musicians through shows and recordings
+ * Like the Oracle of Bacon (six degrees of Kevin Bacon) but for bluegrass!
+ */
+
+import fs from 'fs';
+import yaml from 'js-yaml';
+import path from 'path';
+import { fileURLToPath } from 'url';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+
+class BluegrassOracle {
+ constructor() {
+ this.musicians = new Map(); // musician name -> musician object
+ this.connections = []; // array of connection objects
+ this.graph = new Map(); // musician name -> Set of connected musician names
+ }
+
+ /**
+ * Build the graph from all show and recording data
+ */
+ async buildGraph() {
+ console.log('Building Oracle of Bluegrass Bacon graph...');
+
+ // Load show data
+ await this.loadShowData();
+
+ // Load studio recording data
+ await this.loadStudioData();
+
+ // Build adjacency graph for pathfinding
+ this.buildAdjacencyGraph();
+
+ console.log(`Graph built with ${this.musicians.size} musicians and ${this.connections.length} connections`);
+ return this;
+ }
+
+ /**
+ * Load all show files and extract musician connections
+ */
+ async loadShowData() {
+ const showsDir = path.join(__dirname, '../data/shows');
+ const showFiles = fs.readdirSync(showsDir).filter(f => f.endsWith('.yaml'));
+
+ for (const file of showFiles) {
+ const showPath = path.join(showsDir, file);
+ const showData = yaml.load(fs.readFileSync(showPath, 'utf8'));
+
+ if (!showData || !showData.ensemble) continue;
+
+ // Extract base ensemble musicians
+ const showMusicians = Object.keys(showData.ensemble);
+ this.addMusicians(showMusicians);
+
+ // Create connections between all musicians in this show
+ this.addShowConnections(showMusicians, showData, file);
+
+ // Handle ensemble modifications (featuring musicians)
+ if (showData.sets) {
+ for (const setNum of Object.keys(showData.sets)) {
+ const set = showData.sets[setNum];
+ if (set.songplays) {
+ for (const songplay of set.songplays) {
+ if (typeof songplay === 'object' && songplay['ensemble-modifications']?.featuring) {
+ const featuring = songplay['ensemble-modifications'].featuring;
+ const featuredMusicians = [...showMusicians, ...featuring];
+ this.addMusicians(featuring);
+ this.addShowConnections(featuredMusicians, showData, file, songplay);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Load studio recording data from song files
+ */
+ async loadStudioData() {
+ const songsDir = path.join(__dirname, '../data/songs_and_tunes');
+ const songFiles = fs.readdirSync(songsDir).filter(f => f.endsWith('.yaml'));
+
+ for (const file of songFiles) {
+ const songPath = path.join(songsDir, file);
+ const songData = yaml.load(fs.readFileSync(songPath, 'utf8'));
+
+ if (!songData?.studio_versions) continue;
+
+ // Process each studio version
+ for (const versionNum of Object.keys(songData.studio_versions)) {
+ const version = songData.studio_versions[versionNum];
+
+ // Each version can have multiple albums/releases
+ for (const albumName of Object.keys(version)) {
+ const albumData = version[albumName];
+
+ if (albumData.ensemble) {
+ const studioMusicians = Object.keys(albumData.ensemble);
+ this.addMusicians(studioMusicians);
+ this.addStudioConnections(studioMusicians, albumName, file);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Add musicians to the musicians map
+ */
+ addMusicians(musicianNames) {
+ for (const name of musicianNames) {
+ if (!this.musicians.has(name)) {
+ this.musicians.set(name, {
+ id: this.generateId(name),
+ name: name,
+ connections: new Set()
+ });
+ }
+ }
+ }
+
+ /**
+ * Add connections between musicians who played at the same show
+ */
+ addShowConnections(musicians, showData, filename, songContext = null) {
+ const connectionId = `show-${filename}-${songContext ? 'song' : 'base'}`;
+
+ // Create connections between every pair of musicians
+ for (let i = 0; i < musicians.length; i++) {
+ for (let j = i + 1; j < musicians.length; j++) {
+ const connection = {
+ id: `${connectionId}-${i}-${j}`,
+ musicians: [musicians[i], musicians[j]],
+ type: 'show',
+ context: showData.title || filename,
+ venue: showData.venue,
+ location: showData.locality && showData.region1 ?
+ `${showData.locality}, ${showData.region1}` :
+ showData.locality || showData.region1,
+ blockHeight: filename.split('-')[1]?.split('.')[0], // extract from filename
+ songContext: songContext,
+ artifacts: [] // TODO: populate with set stones, etc.
+ };
+
+ this.connections.push(connection);
+ this.musicians.get(musicians[i]).connections.add(connection.id);
+ this.musicians.get(musicians[j]).connections.add(connection.id);
+ }
+ }
+ }
+
+ /**
+ * Add connections between musicians who recorded together
+ */
+ addStudioConnections(musicians, albumName, filename) {
+ const connectionId = `studio-${filename}-${albumName}`;
+
+ // Create connections between every pair of musicians
+ for (let i = 0; i < musicians.length; i++) {
+ for (let j = i + 1; j < musicians.length; j++) {
+ const connection = {
+ id: `${connectionId}-${i}-${j}`,
+ musicians: [musicians[i], musicians[j]],
+ type: 'recording',
+ context: albumName,
+ song: filename.replace('.yaml', ''),
+ artifacts: [] // TODO: populate with artifacts
+ };
+
+ this.connections.push(connection);
+ this.musicians.get(musicians[i]).connections.add(connection.id);
+ this.musicians.get(musicians[j]).connections.add(connection.id);
+ }
+ }
+ }
+
+ /**
+ * Build adjacency graph for efficient pathfinding
+ */
+ buildAdjacencyGraph() {
+ for (const musician of this.musicians.keys()) {
+ this.graph.set(musician, new Set());
+ }
+
+ for (const connection of this.connections) {
+ const [musician1, musician2] = connection.musicians;
+ this.graph.get(musician1).add(musician2);
+ this.graph.get(musician2).add(musician1);
+ }
+ }
+
+ /**
+ * Find the shortest path between two musicians using BFS
+ */
+ findPath(startMusician, endMusician) {
+ if (!this.musicians.has(startMusician)) {
+ throw new Error(`Musician "${startMusician}" not found in database`);
+ }
+ if (!this.musicians.has(endMusician)) {
+ throw new Error(`Musician "${endMusician}" not found in database`);
+ }
+
+ if (startMusician === endMusician) {
+ return { path: [startMusician], connections: [], degrees: 0 };
+ }
+
+ const queue = [[startMusician]];
+ const visited = new Set([startMusician]);
+
+ while (queue.length > 0) {
+ const currentPath = queue.shift();
+ const currentMusician = currentPath[currentPath.length - 1];
+
+ const neighbors = this.graph.get(currentMusician);
+ for (const neighbor of neighbors) {
+ if (neighbor === endMusician) {
+ const finalPath = [...currentPath, neighbor];
+ const pathConnections = this.getConnectionsForPath(finalPath);
+ return {
+ path: finalPath,
+ connections: pathConnections,
+ degrees: finalPath.length - 1
+ };
+ }
+
+ if (!visited.has(neighbor)) {
+ visited.add(neighbor);
+ queue.push([...currentPath, neighbor]);
+ }
+ }
+ }
+
+ return null; // No path found
+ }
+
+ /**
+ * Get the connections that link musicians in a path
+ */
+ getConnectionsForPath(pathArray) {
+ const pathConnections = [];
+
+ for (let i = 0; i < pathArray.length - 1; i++) {
+ const musician1 = pathArray[i];
+ const musician2 = pathArray[i + 1];
+
+ // Find connection between these two musicians
+ const connection = this.connections.find(conn =>
+ (conn.musicians[0] === musician1 && conn.musicians[1] === musician2) ||
+ (conn.musicians[0] === musician2 && conn.musicians[1] === musician1)
+ );
+
+ if (connection) {
+ pathConnections.push(connection);
+ }
+ }
+
+ return pathConnections;
+ }
+
+ /**
+ * Get all musicians in the database
+ */
+ getAllMusicians() {
+ return Array.from(this.musicians.keys()).sort();
+ }
+
+ /**
+ * Generate a URL-safe ID from a name
+ */
+ generateId(name) {
+ return name.toLowerCase()
+ .replace(/[^a-z0-9\s]/g, '')
+ .replace(/\s+/g, '-');
+ }
+}
+
+export { BluegrassOracle };
+
+// CLI usage
+if (import.meta.url === `file://${process.argv[1]}`) {
+ async function main() {
+ const oracle = new BluegrassOracle();
+ await oracle.buildGraph();
+
+ const args = process.argv.slice(2);
+ if (args.length === 0) {
+ console.log('Available musicians:');
+ console.log(oracle.getAllMusicians().join('\n'));
+ } else if (args.length === 2) {
+ const [start, end] = args;
+ console.log(`\nFinding path from "${start}" to "${end}"...`);
+
+ try {
+ const result = oracle.findPath(start, end);
+ if (result) {
+ console.log(`\n🎵 ${result.degrees} degrees of separation!`);
+ console.log(`Path: ${result.path.join(' → ')}`);
+ console.log('\nConnections:');
+ result.connections.forEach((conn, i) => {
+ console.log(`${i + 1}. ${conn.type === 'show' ? '🎤' : '🎧'} ${conn.context}`);
+ if (conn.venue) console.log(` at ${conn.venue}${conn.location ? `, ${conn.location}` : ''}`);
+ if (conn.songContext) console.log(` during "${Object.keys(conn.songContext)[0]}"`);
+ });
+ } else {
+ console.log('No connection found! 😱');
+ }
+ } catch (error) {
+ console.error(error.message);
+ }
+ } else {
+ console.log('Usage: node oracle_of_bluegrass_bacon.js [musician1] [musician2]');
+ }
+ }
+
+ main().catch(console.error);
+}
\ No newline at end of file
diff --git a/src/build_logic/primary_builder.js b/src/build_logic/primary_builder.js
index c2bf603a..5e2fdf5e 100644
--- a/src/build_logic/primary_builder.js
+++ b/src/build_logic/primary_builder.js
@@ -11,7 +11,7 @@ import nunjucks from "nunjucks";
// Local utilities and helpers
import { getProjectDirs } from "./locations.js";
import { slugify } from "./utils/text_utils.js";
-import { renderPage } from "./utils/rendering_utils.js";
+import { renderPage, generateSitemapXML, generateSitemapHTML, getSitemap, clearSitemap } from "./utils/rendering_utils.js";
import { registerHelpers } from './utils/template_helpers.js';
// Data and asset management
@@ -45,6 +45,9 @@ export const runPrimaryBuild = async () => {
};
ensureDirectories();
console.time('primary-build');
+
+ // Clear sitemap from any previous builds
+ clearSitemap();
// TODO: Do we need to make sure the root output directory exists?
@@ -581,6 +584,24 @@ export const runPrimaryBuild = async () => {
console.warn(`Image not used: ${image}`);
});
+ // Generate sitemaps
+ console.time('sitemap-generation');
+ const sitemap = getSitemap();
+ console.log(`Generated sitemap with ${sitemap.size} pages`);
+
+ // Generate XML sitemap (for search engines - goes in normal location)
+ const baseUrl = site === 'cryptograss.live' ? 'https://cryptograss.live' : 'https://justinholmes.com';
+ const xmlSitemap = generateSitemapXML(site, baseUrl);
+ fs.writeFileSync(path.join(outputPrimarySiteDir, 'sitemap.xml'), xmlSitemap);
+
+ // Generate HTML sitemap (debug tool - goes outside webpack reach)
+ const htmlSitemap = generateSitemapHTML(site);
+ const debugSitemapPath = path.join(outputPrimarySiteDir, `sitemap.html`);
+ fs.writeFileSync(debugSitemapPath, htmlSitemap);
+ console.log(`Debug sitemap written to: ${debugSitemapPath}`);
+
+ console.timeEnd('sitemap-generation');
+
console.timeEnd('primary-build');
return true; // TODO: Return something useful.
}
diff --git a/src/build_logic/utils/rendering_utils.js b/src/build_logic/utils/rendering_utils.js
index cfa34663..fc1a08c1 100644
--- a/src/build_logic/utils/rendering_utils.js
+++ b/src/build_logic/utils/rendering_utils.js
@@ -3,6 +3,9 @@ import fs from "fs";
import { getProjectDirs } from "../locations.js";
import { getNunjucksEnv } from "./template_helpers.js";
+// Sitemap collection - tracks all generated pages
+let sitemap = new Map();
+
export function renderPage({ template_path, context, output_path, layout = "base.njk", site }) {
const { outputPrimaryRootDir, templateDir, basePath } = getProjectDirs();
const outputFilePath = path.join(outputPrimaryRootDir, site, output_path);
@@ -24,5 +27,254 @@ export function renderPage({ template_path, context, output_path, layout = "base
}
fs.writeFileSync(outputFilePath, rendered_page);
+
+ // Add to sitemap
+ let urlPath = output_path.replace(/\.html$/, '').replace(/\/index$/, '') || '/';
+ // Ensure URL path starts with /
+ if (!urlPath.startsWith('/')) {
+ urlPath = '/' + urlPath;
+ }
+ sitemap.set(urlPath, {
+ site: site,
+ template: template_path,
+ outputPath: output_path,
+ fullPath: outputFilePath,
+ title: context.title || context.page_title || context.pageTitle ||
+ (template_path.includes('/') ? template_path.split('/').pop().replace('.njk', '') : 'Untitled'),
+ description: context.description || '',
+ lastModified: new Date().toISOString()
+ });
+
return rendered_page;
+}
+
+export function getSitemap() {
+ return sitemap;
+}
+
+export function clearSitemap() {
+ sitemap.clear();
+}
+
+export function generateSitemapXML(site, baseUrl = 'https://justinholmes.com') {
+ const sitePages = Array.from(sitemap.entries())
+ .filter(([url, data]) => data.site === site)
+ .sort();
+
+ let xml = '\n';
+ xml += '\n';
+
+ for (const [url, data] of sitePages) {
+ xml += ' \n';
+ xml += ` ${baseUrl}${url} \n`;
+ xml += ` ${data.lastModified.split('T')[0]} \n`;
+ xml += ' \n';
+ }
+
+ xml += ' ';
+ return xml;
+}
+
+export function generateSitemapHTML(site) {
+ const sitePages = Array.from(sitemap.entries())
+ .filter(([url, data]) => data.site === site)
+ .sort();
+
+ // Build hierarchy
+ const hierarchy = {};
+
+ for (const [url, data] of sitePages) {
+ const parts = url.split('/').filter(p => p);
+ let current = hierarchy;
+
+ // Build nested structure
+ for (let i = 0; i < parts.length; i++) {
+ const part = parts[i];
+ if (!current[part]) {
+ current[part] = {
+ pages: [],
+ children: {}
+ };
+ }
+ current = current[part].children;
+ }
+
+ // Add page to appropriate level
+ if (parts.length === 0) {
+ // Root level page
+ if (!hierarchy['_root']) {
+ hierarchy['_root'] = { pages: [], children: {} };
+ }
+ hierarchy['_root'].pages.push([url, data]);
+ } else {
+ // Find the right level to add the page
+ let target = hierarchy;
+ for (let i = 0; i < parts.length - 1; i++) {
+ target = target[parts[i]].children;
+ }
+ const lastPart = parts[parts.length - 1];
+ if (!target[lastPart]) {
+ target[lastPart] = { pages: [], children: {} };
+ }
+ target[lastPart].pages.push([url, data]);
+ }
+ }
+
+ function renderHierarchy(obj, level = 0) {
+ let html = '';
+ const indent = ' '.repeat(level);
+
+ for (const [key, value] of Object.entries(obj)) {
+ if (key === '_root') {
+ // Root level pages
+ for (const [url, data] of value.pages) {
+ html += `${indent}
${data.title} `;
+ if (data.description) html += ` - ${data.description}`;
+ html += ` \n`;
+ }
+ continue;
+ }
+
+ const totalItems = value.pages.length + Object.keys(value.children).length;
+
+ if (totalItems === 1 && value.pages.length === 1) {
+ // Single item - display inline
+ const [url, data] = value.pages[0];
+ html += `${indent}${key}/ ${data.title} `;
+ if (data.description) html += ` - ${data.description}`;
+ html += ` \n`;
+ } else if (totalItems > 0) {
+ // Multiple items - collapsible
+ const shouldCollapse = totalItems > 4;
+ const detailsId = `sitemap-${level}-${key}`;
+
+ html += `${indent}\n`;
+ html += `${indent} \n`;
+ html += `${indent} ${key}/ (${totalItems}) \n`;
+ html += `${indent} \n`;
+
+ // Pages at this level
+ for (const [url, data] of value.pages) {
+ html += `${indent} ${data.title} `;
+ if (data.description) html += ` - ${data.description}`;
+ html += ` \n`;
+ }
+
+ // Recurse into children
+ html += renderHierarchy(value.children, level + 3);
+
+ html += `${indent} \n`;
+ html += `${indent} \n`;
+ html += `${indent} \n`;
+ }
+ }
+
+ return html;
+ }
+
+ let html = `
+
+
+
+
+ Sitemap for ${site}
+
+
+
+
+ 🔧 Debug Tool: Site structure generated at ${new Date().toLocaleString()}
+
+ Sitemap for ${site}
+
+${renderHierarchy(hierarchy)}
+
+`;
+
+ return html;
}
\ No newline at end of file
diff --git a/src/build_logic/webpack.common.js b/src/build_logic/webpack.common.js
deleted file mode 100644
index a6b1b099..00000000
--- a/src/build_logic/webpack.common.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import fs from 'fs';
-import { glob } from 'glob';
-import path from 'path';
-import HtmlWebpackPlugin from 'html-webpack-plugin';
-import CopyPlugin from 'copy-webpack-plugin';
-import MiniCssExtractPlugin from 'mini-css-extract-plugin';
-import { getProjectDirs } from './locations.js';
-import { runPrimaryBuild } from './primary_builder.js';
-
-// Check if SKIP_CHAIN_DATA is set
-const skipChainData = process.env.SKIP_CHAIN_DATA;
-
-await runPrimaryBuild(skipChainData);
-
-// Pattern to match all HTML files recursively within the prebuilt directory
-const templatesPattern = path.join(outputPrimaryDir, '**/*.html');
-
-// Use glob to find matching files
-const templateFiles = glob.sync(templatesPattern);
-
-// Create HtmlWebpackPlugin instances
-const htmlPluginInstances = templateFiles.map(templatePath => {
- // Compute the output filename by maintaining the relative directory structure
- // TODO: This is getting a little stretched - let's have a more explicit way to decide the output path.
- const relativePath = path.relative(outputPrimaryDir, templatePath);
-
- // TODO: This is a simply horrible way to decide which scripts to include.
- if (relativePath.startsWith('music/vowel-sounds')) {
- var chunks = ['vowel_sounds'];
- } else if (relativePath.startsWith('sign')) {
- var chunks = ['main', 'signing'];
- } else if (relativePath.startsWith('magichat')) {
- var chunks = ['main', 'magic_hat'];
- } else if (relativePath.startsWith('cryptograss/tools/add-live-set')) {
- var chunks = ['main', 'add_live_set'];
- } else if (relativePath.startsWith('cryptograss/bazaar/setstones')) {
- var chunks = ['main', 'strike_set_stone'];
- } else if (relativePath.startsWith('shows/')) {
- var chunks = ['main', 'strike_set_stone'];
- } else if (relativePath.startsWith('cryptograss/tools/generate_art')) {
- var chunks = ['main', 'shapes'];
- } else if (relativePath.startsWith('cryptograss/tools/add-show-for-stone-minting')) {
- var chunks = ['main', 'add_show_for_stone_minting'];
- } else if (relativePath.startsWith('cryptograss/tools/setstone-color-palette')) {
- var chunks = ['main', 'setstone_color_palette'];
- } else if (relativePath.startsWith('cryptograss/tools/sign-things')) {
- var chunks = ['main', 'signing'];
- } else if (relativePath.startsWith('blue-railroad-test')) {
- var chunks = ['main', 'blue_railroad'];
- } else {
- var chunks = ['main'];
- }
-
- return new HtmlWebpackPlugin({
- template: templatePath, // Path to the source template
- filename: relativePath, // Preserve the directory structure in the output
- inject: "body",
- chunks: chunks, // Only include the chunk for this template
- });
-});
-
-
-const common = {
- output: { path: outputjhcomDistDir },
- plugins: [
- // Copy the .htaccess.
- // Copy assets and such.
- new CopyPlugin({
- patterns: [
- {
- from: path.resolve(outputPrimaryDir, 'assets'),
- to: path.resolve(outputjhcomDistDir, 'assets')
- },
- {
- from: path.resolve(outputPrimaryDir, 'setstones'),
- to: path.resolve(outputjhcomDistDir, 'setstones')
- },
-
- // TODO: Design decision on client partials.
- {
- from: path.resolve(outputPrimaryDir, 'client_partials'),
- to: path.resolve(outputjhcomDistDir, 'partials')
- },
- {
- from: 'src/fetched_assets',
- to: 'assets',
- globOptions: {
- dot: true,
- gitignore: true,
- ignore: ['**/.gitkeep', '**/.DS_Store'],
- },
- noErrorOnMissing: true // Won't error if directory is empty/missing
- },
- ]
- }),
- new MiniCssExtractPlugin({
- filename: '[name].[contenthash].css',
- }),
- ...htmlPluginInstances,
- ],
-
- entry: {
- main: './src/js/index.js',
- vowel_sounds: './src/js/vowel_sounds.js',
- help: './src/js/help.js',
- signing: './src/js/jhmusic_signing.js',
- magic_hat: './src/js/magic_hat.js',
- // strike_set_stone: './src/js/shapes.js',
- strike_set_stone: './src/js/cryptograss/bazaar/strike_set_stones.js',
- add_live_set: './src/js/cryptograss/tools/add_live_set.js',
- add_show_for_stone_minting: './src/js/cryptograss/tools/add_show_for_stone_minting.js',
- shapes: './src/js/shapes.js',
- blue_railroad: './src/js/cryptograss/bazaar/blue_railroad.js',
- },
- module: {
- rules: [
- {
- test: /\.css$/,
- use: [MiniCssExtractPlugin.loader, 'css-loader'],
- },
- ]
- },
-};
-
-export default common;
\ No newline at end of file
diff --git a/src/build_logic/webpack.cryptograss.common.js b/src/build_logic/webpack.cryptograss.common.js
index 32eace72..addebb3a 100644
--- a/src/build_logic/webpack.cryptograss.common.js
+++ b/src/build_logic/webpack.cryptograss.common.js
@@ -13,33 +13,17 @@ const { outputPrimarySiteDir, outputPrimaryRootDir, outputDistDir, siteDir, srcD
// Make sure the output directory exists
fs.mkdirSync(outputDistDir, { recursive: true });
-const templatesPattern = path.join(outputPrimarySiteDir, '**/*.html');
+const templatesPattern = path.join(outputPrimarySiteDir, '**/*.{html,xml}');
const templateFiles = glob.sync(templatesPattern);
const htmlPluginInstances = templateFiles.map(templatePath => {
const relativePath = path.relative(outputPrimarySiteDir, templatePath);
- // if (relativePath.startsWith('cryptograss/tools/add-live-set')) {
- // var chunks = ['main', 'add_live_set'];
- // } else if (relativePath.startsWith('cryptograss/bazaar/setstones')) {
- // var chunks = ['main', 'strike_set_stone'];
- // } else if (relativePath.startsWith('cryptograss/shows/')) {
- // var chunks = ['main', 'strike_set_stone'];
- // } else if (relativePath.startsWith('cryptograss/tools/generate_art')) {
- // var chunks = ['main', 'shapes'];
- // } else if (relativePath.startsWith('cryptograss/tools/add-show-for-stone-minting')) {
- // var chunks = ['main', 'add_show_for_stone_minting'];
- // } else if (relativePath.startsWith('cryptograss/tools/setstone-color-palette')) {
- // var chunks = ['main', 'setstone_color_palette'];
- // } else if (relativePath.startsWith('cryptograss/tools/sign-things')) {
- // var chunks = ['main', 'signing'];
- // } else if (relativePath.startsWith('cryptograss/blue-railroad-test')) {
- // var chunks = ['main', 'blue_railroad'];
- // } else {
-
- // }
-
- var chunks = ['main'];
+ if (relativePath.startsWith('tools/oracle-of-bluegrass-bacon')) {
+ var chunks = ['main', 'oracle_client'];
+ } else {
+ var chunks = ['main'];
+ }
return new HtmlWebpackPlugin({
template: templatePath,
@@ -80,6 +64,16 @@ export default {
},
noErrorOnMissing: true
},
+ {
+ from: path.resolve(siteDir, 'api'),
+ to: path.resolve(outputDistDir, 'api'),
+ noErrorOnMissing: true
+ },
+ {
+ from: path.resolve(srcDir, '../audio'),
+ to: path.resolve(outputDistDir, 'audio'),
+ noErrorOnMissing: true
+ },
]
}),
new MiniCssExtractPlugin({
@@ -94,6 +88,8 @@ export default {
add_show_for_stone_minting: `${frontendJSDir}/tools/add_show_for_stone_minting.js`,
shapes: `${frontendJSDir}/shapes.js`,
blue_railroad: `${frontendJSDir}/bazaar/blue_railroad.js`,
+ oracle_client: `${frontendJSDir}/oracle_client.js`,
+ charitfact_player: `${frontendJSDir}/charitfact_player.js`,
},
module: {
rules: [
diff --git a/src/build_logic/webpack.justinholmes.common.js b/src/build_logic/webpack.justinholmes.common.js
index 4fdb69fc..d33db561 100644
--- a/src/build_logic/webpack.justinholmes.common.js
+++ b/src/build_logic/webpack.justinholmes.common.js
@@ -15,7 +15,7 @@ const { outputDistDir, outputPrimaryRootDir, outputPrimarySiteDir, siteDir, site
// Make sure the output directory exists
fs.mkdirSync(outputDistDir, { recursive: true });
-const templatesPattern = path.join(outputPrimarySiteDir, '**/*.html');
+const templatesPattern = path.join(outputPrimarySiteDir, '**/*.{html,xml}');
const templateFiles = glob.sync(templatesPattern);
const htmlPluginInstances = templateFiles.map(templatePath => {
@@ -32,6 +32,10 @@ const htmlPluginInstances = templateFiles.map(templatePath => {
var chunks = ['main', 'magic_hat'];
} else if (relativePath.startsWith('cryptograss/tools/add-show-for-stone-minting')) {
var chunks = ['main', 'add_show_for_stone_minting'];
+ } else if (relativePath.startsWith('cryptograss/tools/oracle-of-bluegrass-bacon')) {
+ var chunks = ['main', 'oracle_client'];
+ } else if (relativePath.startsWith('songs/') || relativePath.startsWith('songs/')) {
+ var chunks = ['main', 'charitfact_player'];
} else {
var chunks = ['main'];
}
@@ -65,6 +69,16 @@ export default {
},
noErrorOnMissing: true
},
+ {
+ from: path.resolve(siteDir, 'api'),
+ to: path.resolve(outputDistDir, 'api'),
+ noErrorOnMissing: true
+ },
+ {
+ from: path.resolve(srcDir, '../audio'),
+ to: path.resolve(outputDistDir, 'audio'),
+ noErrorOnMissing: true
+ },
]
}),
new MiniCssExtractPlugin({
@@ -81,6 +95,8 @@ export default {
magic_hat: `${frontendJSDir}/magic_hat.js`,
strike_set_stone: `${frontendJSDir}/strike_set_stones.js`,
add_show_for_stone_minting: `${frontendJSDir}/add_show_for_stone_minting.js`,
+ oracle_client: `${frontendJSDir}/oracle_client.js`,
+ charitfact_player: `${frontendJSDir}/charitfact_player.js`,
},
module: {
rules: [
diff --git a/src/data/cryptograss.live.pages.yaml b/src/data/cryptograss.live.pages.yaml
index eab0bee9..e9d06c51 100644
--- a/src/data/cryptograss.live.pages.yaml
+++ b/src/data/cryptograss.live.pages.yaml
@@ -8,3 +8,12 @@ onchain-merch:
include_data_in_context: ["shows", "chainData"]
context:
page_title: "Onchain Merch"
+
+oracle-of-bluegrass-bacon:
+ template: tools/oracle-of-bluegrass-bacon.njk
+ context:
+ title: "Oracle of Bluegrass Bacon"
+ page_title: "Oracle of Bluegrass Bacon"
+ description: "Connect any two musicians through shows and recordings - like Six Degrees of Kevin Bacon, but for bluegrass!"
+ no_video_bg: true
+
diff --git a/src/sites/cryptograss.live/js/oracle_client.js b/src/sites/cryptograss.live/js/oracle_client.js
new file mode 100644
index 00000000..c9e12e7a
--- /dev/null
+++ b/src/sites/cryptograss.live/js/oracle_client.js
@@ -0,0 +1,136 @@
+/**
+ * Client-side Oracle of Bluegrass Bacon pathfinding
+ */
+
+class OracleClient {
+ constructor() {
+ this.musicians = [];
+ this.graph = null;
+ this.connections = [];
+ this.adjacencyList = {};
+ this.loaded = false;
+ }
+
+ async loadData() {
+ if (this.loaded) return;
+
+ try {
+ console.log('Loading Oracle data...');
+
+ // Load musicians list
+ const musiciansResponse = await fetch('/api/oracle/musicians.json');
+ if (!musiciansResponse.ok) {
+ throw new Error('Failed to load musicians data');
+ }
+ this.musicians = await musiciansResponse.json();
+
+ // Load full graph data
+ const graphResponse = await fetch('/api/oracle/graph.json');
+ if (!graphResponse.ok) {
+ throw new Error('Failed to load graph data');
+ }
+
+ const graphData = await graphResponse.json();
+ this.graph = graphData.musicians;
+ this.connections = graphData.connections;
+ this.adjacencyList = graphData.adjacencyList;
+
+ this.loaded = true;
+ console.log('Oracle data loaded:', graphData.stats);
+ } catch (error) {
+ console.error('Error loading Oracle data:', error);
+ throw error;
+ }
+ }
+
+ getMusicians() {
+ return this.musicians;
+ }
+
+ /**
+ * Find shortest path between two musicians using BFS
+ */
+ findPath(startMusician, endMusician) {
+ if (!this.loaded) {
+ throw new Error('Oracle data not loaded');
+ }
+
+ if (!this.adjacencyList[startMusician]) {
+ throw new Error(`Musician "${startMusician}" not found in database`);
+ }
+ if (!this.adjacencyList[endMusician]) {
+ throw new Error(`Musician "${endMusician}" not found in database`);
+ }
+
+ if (startMusician === endMusician) {
+ return { path: [startMusician], connections: [], degrees: 0 };
+ }
+
+ const queue = [[startMusician]];
+ const visited = new Set([startMusician]);
+
+ while (queue.length > 0) {
+ const currentPath = queue.shift();
+ const currentMusician = currentPath[currentPath.length - 1];
+
+ const neighbors = this.adjacencyList[currentMusician] || [];
+ for (const neighbor of neighbors) {
+ if (neighbor === endMusician) {
+ const finalPath = [...currentPath, neighbor];
+ const pathConnections = this.getConnectionsForPath(finalPath);
+ return {
+ path: finalPath,
+ connections: pathConnections,
+ degrees: finalPath.length - 1
+ };
+ }
+
+ if (!visited.has(neighbor)) {
+ visited.add(neighbor);
+ queue.push([...currentPath, neighbor]);
+ }
+ }
+ }
+
+ return null; // No path found
+ }
+
+ /**
+ * Get the connections that link musicians in a path
+ */
+ getConnectionsForPath(pathArray) {
+ const pathConnections = [];
+
+ for (let i = 0; i < pathArray.length - 1; i++) {
+ const musician1 = pathArray[i];
+ const musician2 = pathArray[i + 1];
+
+ // Find connection between these two musicians
+ const connection = this.connections.find(conn =>
+ (conn.musicians[0] === musician1 && conn.musicians[1] === musician2) ||
+ (conn.musicians[0] === musician2 && conn.musicians[1] === musician1)
+ );
+
+ if (connection) {
+ pathConnections.push(connection);
+ }
+ }
+
+ return pathConnections;
+ }
+
+ /**
+ * Search musicians by partial name match
+ */
+ searchMusicians(query) {
+ if (!query || query.length < 2) return [];
+
+ const lowerQuery = query.toLowerCase();
+ return this.musicians
+ .filter(musician => musician.toLowerCase().includes(lowerQuery))
+ .slice(0, 10);
+ }
+}
+
+// Export for use in the page
+window.OracleClient = OracleClient;
\ No newline at end of file
diff --git a/src/sites/cryptograss.live/templates/pages/tools/oracle-of-bluegrass-bacon.njk b/src/sites/cryptograss.live/templates/pages/tools/oracle-of-bluegrass-bacon.njk
new file mode 100644
index 00000000..99f7067f
--- /dev/null
+++ b/src/sites/cryptograss.live/templates/pages/tools/oracle-of-bluegrass-bacon.njk
@@ -0,0 +1,392 @@
+---
+title: "Oracle of Bluegrass Bacon"
+pageId: "oracle-of-bluegrass-bacon"
+description: "Connect any two musicians through shows and recordings - like Six Degrees of Kevin Bacon, but for bluegrass!"
+---
+
+{% extends '../../base.njk' %}
+
+{% block head %}
+
+{% endblock %}
+
+{% block main %}
+
+
🎵 Oracle of Bluegrass Bacon
+
Connect any two musicians through shows and recordings! Like the famous "Six Degrees of Kevin Bacon," but for the cryptograss universe. Find the shortest path between any two musicians who have appeared on our records or performed at our shows.
+
+
+
+
Starting Musician:
+
+
+
+
+
+
+
Find Connection
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/sites/justinholmes.com/js/index.js b/src/sites/justinholmes.com/js/index.js
index 4e7a0cc4..f5410a80 100644
--- a/src/sites/justinholmes.com/js/index.js
+++ b/src/sites/justinholmes.com/js/index.js
@@ -3,6 +3,7 @@ import 'bootstrap';
import 'spotlight.js';
import '../styles/fonts.css';
import '../styles/styles-common.css';
+import '../styles/webamp-chartifacts.css';
import $ from 'jquery';
$(document).ready(function () {
diff --git a/src/sites/justinholmes.com/js/oracle_client.js b/src/sites/justinholmes.com/js/oracle_client.js
new file mode 100644
index 00000000..c9e12e7a
--- /dev/null
+++ b/src/sites/justinholmes.com/js/oracle_client.js
@@ -0,0 +1,136 @@
+/**
+ * Client-side Oracle of Bluegrass Bacon pathfinding
+ */
+
+class OracleClient {
+ constructor() {
+ this.musicians = [];
+ this.graph = null;
+ this.connections = [];
+ this.adjacencyList = {};
+ this.loaded = false;
+ }
+
+ async loadData() {
+ if (this.loaded) return;
+
+ try {
+ console.log('Loading Oracle data...');
+
+ // Load musicians list
+ const musiciansResponse = await fetch('/api/oracle/musicians.json');
+ if (!musiciansResponse.ok) {
+ throw new Error('Failed to load musicians data');
+ }
+ this.musicians = await musiciansResponse.json();
+
+ // Load full graph data
+ const graphResponse = await fetch('/api/oracle/graph.json');
+ if (!graphResponse.ok) {
+ throw new Error('Failed to load graph data');
+ }
+
+ const graphData = await graphResponse.json();
+ this.graph = graphData.musicians;
+ this.connections = graphData.connections;
+ this.adjacencyList = graphData.adjacencyList;
+
+ this.loaded = true;
+ console.log('Oracle data loaded:', graphData.stats);
+ } catch (error) {
+ console.error('Error loading Oracle data:', error);
+ throw error;
+ }
+ }
+
+ getMusicians() {
+ return this.musicians;
+ }
+
+ /**
+ * Find shortest path between two musicians using BFS
+ */
+ findPath(startMusician, endMusician) {
+ if (!this.loaded) {
+ throw new Error('Oracle data not loaded');
+ }
+
+ if (!this.adjacencyList[startMusician]) {
+ throw new Error(`Musician "${startMusician}" not found in database`);
+ }
+ if (!this.adjacencyList[endMusician]) {
+ throw new Error(`Musician "${endMusician}" not found in database`);
+ }
+
+ if (startMusician === endMusician) {
+ return { path: [startMusician], connections: [], degrees: 0 };
+ }
+
+ const queue = [[startMusician]];
+ const visited = new Set([startMusician]);
+
+ while (queue.length > 0) {
+ const currentPath = queue.shift();
+ const currentMusician = currentPath[currentPath.length - 1];
+
+ const neighbors = this.adjacencyList[currentMusician] || [];
+ for (const neighbor of neighbors) {
+ if (neighbor === endMusician) {
+ const finalPath = [...currentPath, neighbor];
+ const pathConnections = this.getConnectionsForPath(finalPath);
+ return {
+ path: finalPath,
+ connections: pathConnections,
+ degrees: finalPath.length - 1
+ };
+ }
+
+ if (!visited.has(neighbor)) {
+ visited.add(neighbor);
+ queue.push([...currentPath, neighbor]);
+ }
+ }
+ }
+
+ return null; // No path found
+ }
+
+ /**
+ * Get the connections that link musicians in a path
+ */
+ getConnectionsForPath(pathArray) {
+ const pathConnections = [];
+
+ for (let i = 0; i < pathArray.length - 1; i++) {
+ const musician1 = pathArray[i];
+ const musician2 = pathArray[i + 1];
+
+ // Find connection between these two musicians
+ const connection = this.connections.find(conn =>
+ (conn.musicians[0] === musician1 && conn.musicians[1] === musician2) ||
+ (conn.musicians[0] === musician2 && conn.musicians[1] === musician1)
+ );
+
+ if (connection) {
+ pathConnections.push(connection);
+ }
+ }
+
+ return pathConnections;
+ }
+
+ /**
+ * Search musicians by partial name match
+ */
+ searchMusicians(query) {
+ if (!query || query.length < 2) return [];
+
+ const lowerQuery = query.toLowerCase();
+ return this.musicians
+ .filter(musician => musician.toLowerCase().includes(lowerQuery))
+ .slice(0, 10);
+ }
+}
+
+// Export for use in the page
+window.OracleClient = OracleClient;
\ No newline at end of file
diff --git a/src/sites/justinholmes.com/js/webamp_chartifacts.js b/src/sites/justinholmes.com/js/webamp_chartifacts.js
new file mode 100644
index 00000000..943d8d66
--- /dev/null
+++ b/src/sites/justinholmes.com/js/webamp_chartifacts.js
@@ -0,0 +1,532 @@
+// /**
+// * Webamp Chartifacts Integration - Prototype
+// * Combines Webamp player with chartifacts solo highlighting and NFT display
+// */
+
+import Webamp from 'webamp';
+
+class WebampChartifacts {
+ constructor(containerId, trackData) {
+ this.container = document.getElementById(containerId);
+ this.trackData = trackData;
+ this.webamp = null;
+ this.currentSolo = null;
+ this.timeUpdateInterval = null;
+ this.currentEra = null; // Track current era for state persistence
+ this.allMomentTimes = this.extractMomentTimes(); // All moments in the track
+ this.upcomingMomentTimes = [...this.allMomentTimes]; // Moments that haven't fired yet
+ this.flashInProgress = false; // Track if a flash effect is happening
+
+ // Inject dynamic styles if color scheme is provided
+ this.injectDynamicStyles();
+
+ this.init();
+ }
+
+ injectDynamicStyles() {
+ // Only inject styles if a color scheme is provided
+ if (!this.trackData.colorScheme) return;
+
+ const styleId = `webamp-chartifacts-dynamic-styles`;
+ // Remove existing dynamic styles if they exist
+ const existingStyle = document.getElementById(styleId);
+ if (existingStyle) {
+ existingStyle.remove();
+ }
+
+ const colors = this.trackData.colorScheme; }
+
+ async init() {
+ this.renderSimpleUI();
+ await this.initWebamp();
+ this.setupTimeTracking();
+ }
+
+ renderSimpleUI() {
+ // Add ensemble display alongside Webamp
+ const ensembleDiv = document.getElementById('ensemble-display');
+ const partsChart = document.getElementById('parts-chart');
+
+ // Extract song parts from timeline
+ const songParts = this.extractSongParts();
+ songParts.forEach((part, index) => {
+ const partBox = document.createElement('div');
+ partBox.id = `part-${index}`;
+ partBox.className = 'part-box';
+ partBox.textContent = part;
+ partsChart.appendChild(partBox);
+ });
+
+ // Create ensemble list
+ const ensembleList = document.createElement('div');
+ ensembleList.style.display = 'flex';
+ ensembleList.style.flexDirection = 'column';
+
+ Object.entries(this.trackData.ensemble).forEach(([musicianName, musicianData]) => {
+ const musicianDiv = document.createElement('div');
+ musicianDiv.id = `musician-${musicianName.replace(/\s+/g, '-').toLowerCase()}`;
+ musicianDiv.className = 'musician-card';
+ musicianDiv.innerHTML = `
+ ${musicianName} (${musicianData.defaultInstrument})
+ Chartifact "0x1234ff" owned by cryptograss.eth
+ `;
+ ensembleList.appendChild(musicianDiv);
+ });
+
+ ensembleDiv.appendChild(ensembleList);
+ }
+
+
+ renderSolosList() {
+ return this.trackData.solos.map(solo => `
+
+
+
${solo.description}
+
Chartifact #${solo.chartifactTokenId}
+
+ `).join('');
+ }
+
+ async initWebamp() {
+ // Create Webamp instance with our track
+ this.webamp = new Webamp({
+ zindex: 9999,
+ windowLayout: {
+ main: {
+ position: { top: 0, left: 0 },
+ shadeMode: false,
+ closed: false,
+ },
+ },
+ initialTracks: [{
+ url: this.trackData.audioFile,
+ }],
+ enableHotkeys: true,
+ volume: 75
+ });
+
+ // Render Webamp directly to the main container - no custom HTML
+ this.webamp.renderWhenReady(this.container).then(() => {
+ console.log(`rendered webamp in ${this.container}`);
+ // Set up event listeners for Webamp
+ this.setupWebampListeners();
+ }).catch(error => {
+ alert(error.message)
+ });
+
+ }
+
+ setupWebampListeners() {
+ // Listen for play/pause events
+ this.webamp.onWillClose(() => {
+ console.log('Webamp closing');
+ this.cleanup();
+ });
+
+ // Access Webamp's Redux store to monitor playback state
+ const unsubscribe = this.webamp.store.subscribe(() => {
+ const state = this.webamp.store.getState();
+ const { timeElapsed, status } = state.media;
+
+ if (status === 'PLAYING') {
+ if (!this.timeUpdateInterval) {
+ this.startTimeTracking();
+ }
+ } else {
+ this.stopTimeTracking();
+ // Reset ensemble display when not playing
+ this.resetEnsembleDisplay();
+ }
+ });
+
+ this.unsubscribeWebamp = unsubscribe;
+ }
+
+ setupTimeTracking() {
+ // Simple time tracking for status updates
+ console.log('Webamp ready for interaction');
+ }
+
+ startTimeTracking() {
+ this.timeUpdateInterval = setInterval(() => {
+ if (this.webamp && this.webamp.store) {
+ const state = this.webamp.store.getState();
+ const timeElapsed = state.media.timeElapsed;
+
+ if (timeElapsed !== undefined) {
+ this.updateStatusDisplay(timeElapsed);
+ }
+ else {
+ // What here? Throw an error?
+ console.error("Time wasn't defined. Weird.")
+ }
+ }
+ }, 100); // Update every 100ms for smooth updates
+ }
+
+ updateStatusDisplay(currentTime) {
+ this.updateEnsembleDisplay(currentTime);
+ }
+
+ extractMomentTimes() {
+ // Find all timeline entries that have moment-worthy details
+ const momentTimes = [];
+ Object.entries(this.trackData.timeline).forEach(([timeStr, arrangement]) => {
+ const time = Number(timeStr);
+ if (arrangement.detail === "band-in" && time > 0) { // Exclude time 0
+ momentTimes.push(time);
+ }
+ // Add other moment types here as needed
+ });
+ return momentTimes.sort((a, b) => a - b);
+ }
+
+ extractSongParts() {
+ // Build chronological sequence of parts from timeline
+ const partSequence = [];
+ const timelineEntries = Object.entries(this.trackData.timeline)
+ .map(([timeStr, arrangement]) => ({ time: Number(timeStr), arrangement }))
+ .sort((a, b) => a.time - b.time);
+
+ timelineEntries.forEach(({ arrangement }) => {
+ if (arrangement.part) {
+ partSequence.push(arrangement.part);
+ }
+ });
+
+ return partSequence;
+ }
+
+ updatePartsChart(arrangement) {
+ const currentPart = arrangement ? arrangement.part : null;
+ const currentPartIndex = this.getCurrentPartIndex();
+ const songParts = this.extractSongParts();
+
+ songParts.forEach((part, index) => {
+ const partBox = document.getElementById(`part-${index}`);
+ if (!partBox) return;
+
+ const isActivePartIndex = index === currentPartIndex;
+
+ if (isActivePartIndex) {
+ partBox.classList.add('active');
+ } else {
+ partBox.classList.remove('active');
+ }
+ });
+ }
+
+ getCurrentPartIndex() {
+ // Find the most recent part change in chronological order
+ const currentTime = this.getCurrentTime();
+ const timelineEntries = Object.entries(this.trackData.timeline)
+ .map(([timeStr, arrangement]) => ({ time: Number(timeStr), arrangement }))
+ .filter(({ arrangement }) => arrangement.part) // Only entries with parts
+ .sort((a, b) => a.time - b.time);
+
+ // Find the last part change that has occurred
+ let activePartIndex = -1;
+ timelineEntries.forEach(({ time, arrangement }, chronologicalIndex) => {
+ if (time <= currentTime) {
+ activePartIndex = chronologicalIndex;
+ }
+ });
+
+ return activePartIndex;
+ }
+
+ updateMusicianCard(musicianDiv, musicianName, instrument, classes = [], showStar = false) {
+ // Clear all state classes
+ musicianDiv.className = 'musician-card';
+
+ // Add any state classes
+ classes.forEach(cls => musicianDiv.classList.add(cls));
+
+ // Update content
+ const starPrefix = showStar ? '⭐ ' : '';
+ musicianDiv.innerHTML = `
+ ${starPrefix}${musicianName} (${instrument})
+ Chartifact "0x1234ff" owned by cryptograss.eth
+ `;
+ }
+
+ getCurrentArrangement(currentTime) {
+ const startTimes = Object.keys(this.trackData.timeline).map(Number).sort((a, b) => b - a);
+ const currentStartTime = startTimes.find(time => time <= currentTime);
+ return this.trackData.timeline[currentStartTime];
+ }
+
+ getCurrentInstruments(currentTime) {
+ const arrangement = this.getCurrentArrangement(currentTime);
+ const instruments = {};
+
+ // Start with default instruments
+ Object.entries(this.trackData.ensemble).forEach(([musician, musicianData]) => {
+ instruments[musician] = musicianData.defaultInstrument;
+ });
+
+ // Apply any instrument changes for current timeline segment
+ if (arrangement && arrangement.instrumentChanges) {
+ Object.entries(arrangement.instrumentChanges).forEach(([musician, instrument]) => {
+ instruments[musician] = instrument;
+ });
+ }
+
+ return instruments;
+ }
+
+ updateEnsembleDisplay(currentTime) {
+ ////
+ /// This is the big thing that happens every 100ms
+ ////
+ const arrangement = this.getCurrentArrangement(currentTime);
+ const currentInstruments = this.getCurrentInstruments(currentTime);
+ const currentSoloist = arrangement ? arrangement.soloist : null;
+
+ // Handle moments (one-time triggers)
+ this.checkForMoments(currentTime);
+
+ // Track current era for state persistence
+ if (arrangement !== this.currentEra) {
+ this.currentEra = arrangement;
+ }
+
+ // Update parts chart
+ this.updatePartsChart(arrangement);
+
+ // Skip normal ensemble updates during flash effects
+ if (this.flashInProgress) {
+ return;
+ }
+
+ Object.entries(this.trackData.ensemble).forEach(([musicianName, musicianData]) => {
+ const musicianDiv = document.getElementById(`musician-${musicianName.replace(/\s+/g, '-').toLowerCase()}`);
+ if (!musicianDiv) return;
+
+ const instrument = currentInstruments[musicianName];
+
+ // Handle different lead types
+ if (currentSoloist === musicianName) {
+ const leadType = arrangement ? arrangement.type : null;
+
+ if (leadType === 'solo') {
+ this.updateMusicianCard(musicianDiv, musicianName, instrument, ['solo'], true);
+ } else if (leadType === 'pick up') {
+ this.updateMusicianCard(musicianDiv, musicianName, instrument, ['pickup']);
+ } else {
+ // Intro or default
+ this.updateMusicianCard(musicianDiv, musicianName, instrument, ['intro']);
+ }
+ } else {
+ // Not leading - check if we need to gray out during intro
+ const leadType = arrangement ? arrangement.type : null;
+ const isIntroScene = currentSoloist && leadType === 'intro';
+
+ if (isIntroScene) {
+ this.updateMusicianCard(musicianDiv, musicianName, instrument, ['grayed']);
+ } else {
+ this.updateMusicianCard(musicianDiv, musicianName, instrument, []);
+ }
+ }
+ });
+ }
+
+ flashEntireEnsemble() {
+ // Set flash in progress to prevent normal updates from interfering
+ this.flashInProgress = true;
+
+ // Flash each musician individually with golden highlight
+ Object.entries(this.trackData.ensemble).forEach(([musicianName]) => {
+ const musicianDiv = document.getElementById(`musician-${musicianName.replace(/\s+/g, '-').toLowerCase()}`);
+ if (!musicianDiv) return;
+
+ // Apply flash class
+ musicianDiv.classList.add('flash');
+ });
+
+ // Fade back to normal after 1.5 seconds
+ setTimeout(() => {
+ // Remove flash class from all musicians
+ Object.entries(this.trackData.ensemble).forEach(([musicianName]) => {
+ const musicianDiv = document.getElementById(`musician-${musicianName.replace(/\s+/g, '-').toLowerCase()}`);
+ if (!musicianDiv) return;
+ musicianDiv.classList.remove('flash');
+ });
+
+ // Reset flash effect, let normal updates resume
+ this.flashInProgress = false;
+ }, 300);
+ }
+
+ checkForMoments(currentTime) {
+ // Sort all upcoming moments into three buckets
+ const momentsNotYetReached = [];
+ const momentsToTrigger = [];
+ const momentsMissedAndNeedCulling = [];
+
+ this.upcomingMomentTimes.forEach(momentTime => {
+ const secondsSinceMoment = currentTime - momentTime;
+
+ if (secondsSinceMoment < 0) {
+ // Future moment
+ momentsNotYetReached.push(momentTime);
+ } else if (secondsSinceMoment <= 1) {
+ // Recent enough to trigger
+ momentsToTrigger.push(momentTime);
+ } else {
+ // Too old, cull it
+ momentsMissedAndNeedCulling.push(momentTime);
+ }
+ });
+
+ // Trigger the ready moments
+ momentsToTrigger.forEach(momentTime => {
+ this.triggerMoment(momentTime);
+ });
+
+ // Update upcoming list to only include future moments
+ this.upcomingMomentTimes = momentsNotYetReached;
+ }
+
+ triggerMoment(momentTime) {
+ const arrangement = this.trackData.timeline[momentTime];
+ if (!arrangement) return;
+
+ console.log(`Triggering moment at ${momentTime}:`, arrangement);
+
+ // Handle different moment types
+ if (arrangement.detail === "band-in") {
+ this.flashEntireEnsemble();
+ }
+ // Add other moment types here as needed
+ }
+
+ // Remove showPickupEffect since pickup is now an era, not a moment
+
+ getCurrentTime() {
+ if (this.webamp && this.webamp.store) {
+ const state = this.webamp.store.getState();
+ return state.media.timeElapsed || 0;
+ }
+ return 0;
+ }
+
+ stopTimeTracking() {
+ if (this.timeUpdateInterval) {
+ clearInterval(this.timeUpdateInterval);
+ this.timeUpdateInterval = null;
+ }
+ }
+
+ updateCurrentSolo(currentTime) {
+ const currentSolo = this.trackData.solos.find(solo =>
+ currentTime >= solo.startTime && currentTime <= solo.endTime
+ );
+
+ if (currentSolo !== this.currentSolo) {
+ this.currentSolo = currentSolo;
+ this.displayCurrentSolo();
+ this.highlightTimelineMarker();
+ }
+ }
+
+ // displayCurrentSolo() {
+ // const currentSoloDiv = document.getElementById('currentSolo');
+ // const chartDisplay = document.getElementById('chartDisplay');
+
+ // if (this.currentSolo) {
+ // currentSoloDiv.innerHTML = `
+ //
+ //
🎵 ${this.currentSolo.musician}
+ //
${this.currentSolo.instrument} • ${this.currentSolo.description}
+ //
+ // `;
+
+ // // Show chart if available
+ // if (this.currentSolo.chartImage) {
+ // document.getElementById('chartImage').src = this.currentSolo.chartImage;
+ // document.getElementById('tokenId').textContent = this.currentSolo.chartifactTokenId;
+ // chartDisplay.style.display = 'block';
+
+ // // Load NFT ownership (mock for now)
+ // this.loadChartifactOwnership(this.currentSolo.chartifactTokenId);
+ // }
+ // } else {
+ // currentSoloDiv.innerHTML = `
+ //
+ //
Track playing...
+ //
Background accompaniment
+ //
+ // `;
+ // chartDisplay.style.display = 'none';
+ // }
+ // }
+
+ highlightTimelineMarker() {
+ // Remove previous highlighting
+ document.querySelectorAll('.timeline-marker').forEach(marker => {
+ marker.classList.remove('active');
+ });
+
+ // Add highlighting to current solo marker
+ if (this.currentSolo) {
+ const activeMarker = document.querySelector(`[data-musician="${this.currentSolo.musician}"][data-instrument="${this.currentSolo.instrument}"]`);
+ if (activeMarker) {
+ activeMarker.classList.add('active');
+ }
+ }
+ }
+
+ seekTo(timeSeconds) {
+ if (this.webamp && this.webamp.store) {
+ // Use Webamp's action to seek
+ const timeMs = timeSeconds * 1000;
+ this.webamp.store.dispatch({
+ type: 'SEEK_TO_PERCENT_COMPLETE',
+ percent: (timeSeconds / this.trackData.duration) * 100
+ });
+ }
+ }
+
+ async loadChartifactOwnership(tokenId) {
+ // Mock ownership data - in production this would query the blockchain
+ const mockOwners = {
+ 100: "0x1234...5678",
+ 101: "0xabcd...ef01",
+ 102: "0x9876...5432",
+ 103: "0xdef0...1234"
+ };
+
+ const ownerAddress = mockOwners[tokenId] || "Unclaimed";
+ document.getElementById('ownerAddress').textContent = ownerAddress;
+ }
+
+ formatTime(seconds) {
+ const mins = Math.floor(seconds / 60);
+ const secs = Math.floor(seconds % 60);
+ return `${mins}:${secs.toString().padStart(2, '0')}`;
+ }
+
+ resetEnsembleDisplay() {
+ Object.entries(this.trackData.ensemble).forEach(([musicianName, musicianData]) => {
+ const musicianDiv = document.getElementById(`musician-${musicianName.replace(/\s+/g, '-').toLowerCase()}`);
+ if (!musicianDiv) return;
+
+ // Use helper function to reset to default state
+ this.updateMusicianCard(musicianDiv, musicianName, musicianData.defaultInstrument);
+ });
+ }
+
+ cleanup() {
+ this.stopTimeTracking();
+ if (this.unsubscribeWebamp) {
+ this.unsubscribeWebamp();
+ }
+ }
+}
+
+// Export for use in pages
+window.WebampChartifacts = WebampChartifacts;
\ No newline at end of file
diff --git a/src/sites/justinholmes.com/styles/webamp-chartifacts.css b/src/sites/justinholmes.com/styles/webamp-chartifacts.css
new file mode 100644
index 00000000..15b779fc
--- /dev/null
+++ b/src/sites/justinholmes.com/styles/webamp-chartifacts.css
@@ -0,0 +1,139 @@
+/* Webamp Chartifacts Player Styles */
+
+#webamp {
+ z-index: 9999 !important;
+}
+
+/* Import handwritten font */
+@import url('https://fonts.googleapis.com/css2?family=Caveat:wght@400..700&display=swap');
+
+/* Parts chart styling */
+.part-box {
+ padding: 3px 8px;
+ border: 1px solid #ccc;
+ background: #f8f9fa;
+ border-radius: 3px;
+ font-size: 11px;
+ font-weight: bold;
+ color: #666;
+ font-family: 'Caveat', cursive, sans-serif;
+ transition: all 0.3s ease;
+}
+
+.part-box.active {
+ padding: 6px 13px;
+ background: #000000;
+ font-size: 22px;
+ color: white;
+}
+
+/* Musician card styling */
+.musician-card {
+ padding: 4px 6px;
+ margin: 2px 0;
+ background: #f8f9fa;
+ transition: all 0.3s ease;
+ line-height: 1.2;
+ transform: scale(1);
+ border-radius: 4px;
+ border: 1px solid #ddd;
+}
+
+.musician-card.solo {
+ border: 2px solid #000000;
+ background: #e3f2fd;
+ transform: scale(1.05);
+ animation: pulse-glow 3s infinite ease-in-out;
+ order: -1;
+}
+
+@keyframes pulse-glow {
+ 0%, 100% {
+ box-shadow: 0 0 3px 2px rgba(255, 163, 72, 0.75);
+ }
+ 50% {
+ box-shadow: 0 0 11px 3px rgba(255, 163, 72, 0);
+ }
+}
+
+.musician-card.solo .chartifact-line {
+ color: #0056b3 !important;
+}
+
+.musician-card.pickup {
+ border: 2px solid #ff9800;
+ background: #fff3e0;
+ transform: scale(1.02);
+}
+
+.musician-card.pickup .chartifact-line {
+ color: #e65100 !important;
+}
+
+.musician-card.intro {
+ border: 1px solid #17a2b8;
+ background: #e6f7ff;
+ transform: scale(1.02);
+}
+
+.musician-card.intro .chartifact-line {
+ color: #117a8b !important;
+}
+
+.musician-card.grayed {
+ border: 1px solid #e0e0e0;
+ background: #f5f5f5;
+ opacity: 0.5;
+}
+
+.musician-card.grayed .chartifact-line {
+ color: #bbb !important;
+}
+
+.musician-card.grayed .musician-name {
+ font-weight: normal !important;
+ color: #999 !important;
+}
+
+.musician-card.flash {
+ box-shadow: 0 0 2px #ffd700;
+ font-weight: bold;
+}
+
+/* Webamp container styling */
+#just-the-player-goes-here {
+ height: 116px;
+}
+
+/* Ensemble display - fixed position UI element */
+#ensemble-display {
+ padding: 20px;
+ border-radius: 8px;
+ border: 1px solid #dee2e6;
+ font-family: Arial, sans-serif;
+ font-size: 27px;
+ color: #333;
+ z-index: 1000;
+ min-width: 250px;
+}
+
+/* Parts chart container */
+#parts-chart {
+ margin-bottom: 8px;
+ padding: 4px;
+ height: 20px;
+}
+
+/* Musician name styling */
+.musician-name {
+ font-family: 'Caveat', cursive, sans-serif;
+ font-weight: bold;
+ margin-bottom: 1px;
+}
+
+/* Chartifact ownership line */
+.chartifact-line {
+ font-size: 9px;
+ color: #666;
+ font-family: 'JetBrains Mono', monospace;
+}
\ No newline at end of file
diff --git a/src/sites/justinholmes.com/templates/pages/cryptograss/tools/oracle-of-bluegrass-bacon.njk b/src/sites/justinholmes.com/templates/pages/cryptograss/tools/oracle-of-bluegrass-bacon.njk
new file mode 100644
index 00000000..2c40a563
--- /dev/null
+++ b/src/sites/justinholmes.com/templates/pages/cryptograss/tools/oracle-of-bluegrass-bacon.njk
@@ -0,0 +1,376 @@
+---
+title: "Oracle of Bluegrass Bacon"
+pageId: "oracle-of-bluegrass-bacon"
+description: "Connect any two musicians through shows and recordings - like Six Degrees of Kevin Bacon, but for bluegrass!"
+---
+
+{% extends "shared/layouts/base.njk" %}
+
+{% block head %}
+
+{% endblock %}
+
+{% block content %}
+
+
🎵 Oracle of Bluegrass Bacon
+
Connect any two musicians through shows and recordings! Like the famous "Six Degrees of Kevin Bacon," but for the cryptograss universe. Find the shortest path between any two musicians who have appeared on our records or performed at our shows.
+
+
+
+
Starting Musician:
+
+
+
+
+
+
+
Find Connection
+
+
+
+
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/src/sites/justinholmes.com/templates/reuse/single-song.njk b/src/sites/justinholmes.com/templates/reuse/single-song.njk
index 3942f6d6..00581d59 100644
--- a/src/sites/justinholmes.com/templates/reuse/single-song.njk
+++ b/src/sites/justinholmes.com/templates/reuse/single-song.njk
@@ -1,106 +1,187 @@
{% extends 'shared/layouts/base.njk' %}
+
+{# Check if any studio version has chartifacts data #}
+{% set chartifacts_version = null %}
+{% for version_num, version in song.studio_versions if not chartifacts_version %}
+{% for release_name, release_data in version if not chartifacts_version %}
+{% if release_data.chartifacts %}
+{% set chartifacts_version = release_data %}
+{% endif %}
+{% endfor %}
+{% endfor %}
+
+{% block additional_css %}
+{% if chartifacts_version and chartifacts_version.chartifacts %}
+
+{% endif %}
+{% endblock %}
+
{% block main %}
- {% include 'partials/bigger-banner.njk' %}
-
-
-
-
-
-
{{ song.title }}
- {% if song.traditional %}
-
(traditional)
- {% endif %}
- {% if song.by_artist %}
-
by {{ song.by_artist }}
- {% endif %}
- {% if song.video_game %}
-
(from {{ song.video_game }})
- {% endif %}
- {% if song.quick_facts %}
-
- {% for fact_name, fact_value in song.quick_facts %}
- {{ fact_name }}: {{ fact_value }}
- {% endfor %}
-
- {% endif %}
- {% if commentary %}
-
Justin's Commentary
-
- {{ commentary |safe }}
-
- {% endif %}
-
-
Set Stones
-
-
- {% if song.other_resources %}
-
Other resources
-
- {% for resource_name, other_resource_url in song.other_resources %}
-
- {{ resource_name }}
-
- {% endfor %}
-
- {% endif %}
+{% include 'partials/bigger-banner.njk' %}
+
+
+
+
+
+
{{ song.title }}
+ {% if song.traditional %}
+
(traditional)
+ {% endif %}
+ {% if song.by_artist %}
+
by {{ song.by_artist }}
+ {% endif %}
+ {% if song.video_game %}
+
(from {{ song.video_game }})
+ {% endif %}
+ {% if song.quick_facts %}
+
+ {% for fact_name, fact_value in song.quick_facts %}
+ {{ fact_name }}: {{ fact_value }}
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% if chartifacts_version and chartifacts_version.chartifacts %}
+
+
+
+ Loading interactive player...
+
+
+
-
- {% set instrument_to_show = song.instrumentalist_to_display %}
- {% include 'partials/song-all-plays.njk' %}
-
- {% if song.selected_versions %}
-
Selected versions
-
- {% for media in song.selected_versions %}
-
-
{{ media.artist }}
-
- {% if media.youtube %}
- {% set url = media.youtube %}
- {% include 'partials/youtube.njk' %}
- {% endif %}
-
-
+
+
+
+ {% endif %}
+
+
+
+
+ {% if commentary %}
+
Justin's Commentary
+
+ {{ commentary |safe }}
+
+ {% endif %}
+
+
+
Set Stones
+
+
+ {% if song.other_resources %}
+
Other resources
+
+ {% for resource_name, other_resource_url in song.other_resources %}
+
+ {{ resource_name }}
+
+ {% endfor %}
+
+ {% endif %}
+
+
+ {% set instrument_to_show = song.instrumentalist_to_display %}
+ {% include 'partials/song-all-plays.njk' %}
+
+ {% if song.selected_versions %}
+
Selected versions
+
+ {% for media in song.selected_versions %}
+
+
{{ media.artist }}
+
+ {% if media.youtube %}
+ {% set url = media.youtube %}
+ {% include 'partials/youtube.njk' %}
+ {% endif %}
+
+
+ {% endfor %}
+
+ {% endif %}
+
+
+
{% endblock %}
\ No newline at end of file