Database
Our application employs a hybrid database architecture, combining the strengths of a traditional SQL database (SQLite3) with JSON file storage. This approach ensures structured query capabilities while maintaining human-readable data persistence. JSON provides a reliable and platform-independent format for storage, inspection, and backup, whereas SQLite offers efficient querying, indexing, and relationship management.
Key Technologies and Libraries
The database implementation relies on the following technologies:
- JSON Files: The primary storage format for song data, providing human-readable data persistence
- SQLite3: A lightweight, serverless SQL database engine used as a runtime database
- Sequelize ORM: An object-relational mapping library for structured database interactions
- Electron IPC: For secure communication between UI and database operations
- Node.js fs/path modules: For file system operations and path management
Database Architecture
JSON
The core of our database system is the collection of JSON files stored in resources/assets/songData/
. Each song is represented by a dedicated JSON file with a filename matching the song's ID:
The model is defined using Sequelize's type system, which provides runtime validation and type-checking during database operations:
{
"id": "0qj67KE5VXI",
"title": "Troy, Gabriella - Breaking Free (From \"High School Musical\")",
"uploader": "DisneyMusicVEVO",
"audioPath": "audio/0qj67KE5VXI.mp3",
"jacket": "images/0qj67KE5VXI/jacket.png",
"images": [
"images/0qj67KE5VXI/image_1741452244800.png"
],
"moods": [],
"status": "Yellow",
// ...other fields...
}
These JSON files serve as the core for our application data, containing all metadata and configuration for each song.
SQLite as Runtime Database
When the application starts, it reads all JSON files from the songData
directory and loads them into a SQLite database using Sequelize ORM:
// ...existing code...
// Read in json files from assets/songData
const songDataDir = path.join(app.getAppPath(), 'resources', 'assets', 'songData');
const songFiles = fs
.readdirSync(songDataDir)
.filter((file) => file.endsWith('.json'));
const sampleSongs = songFiles.map((file) => {
const filePath = path.join(songDataDir, file);
const songData = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
return songData;
});
// Add songs to SQLite database
const createdSongs = await Song.bulkCreate(sampleSongs);
// ...existing code...
This SQLite database provides:
- Efficient querying and filtering
- Transaction support
- Relationship management
- A structured interface for application code
Bidirectional Synchronisation
Whenever song metadata is modified in the application, updates are reflected in both JSON files and the SQLite database. This is handled by saveSongAsJson
:
// ...existing code...
// Function to save a Song instance as a JSON file
const saveSongAsJson = async (song: SongModel) => {
const songData = {
id: song.id,
title: song.title,
// ...other fields...
};
const songDataDir = getResourcePath('assets', 'songData');
if (!fs.existsSync(songDataDir)) {
fs.mkdirSync(songDataDir, { recursive: true });
}
const filePath = path.join(songDataDir, `${song.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(songData, null, 2));
console.log(`Song saved as JSON file: ${filePath}`);
};
// ...existing code...
Data Model
The Song
model defines the structure for both the SQLite database and the JSON files:
// ...existing code...
interface SongModel
extends Model<
InferAttributes<SongModel>,
InferCreationAttributes<SongModel>
> {
id: CreationOptional<string>;
title: string;
uploader: string;
audioPath: string;
jacket: string;
images: string[];
moods: string[];
status: string;
colours: string[];
colours_reason: string[];
objects: string[];
object_prompts: string[];
particles: string[];
backgrounds: string[];
background_prompts: string[];
shaderBackground: string;
shaderTexture: string;
particleColour: string[];
createdAt: CreationOptional<Date>;
updatedAt: CreationOptional<Date>;
}
// ...existing code...
This comprehensive model supports:
- Basic metadata: Title, artist, creation date
- Asset references: Paths to audio files, images, and visualisations
- Status tracking: Processing state of each song (Blue, Yellow, Green, Red)
- AI-generated content: Colors, moods, particles, backgrounds
- Customisation options: User-defined settings and preferences
Database Configuration
The SQLite database is configured in config.ts
:
// ...existing code...
const dbPath = path.join(app.getPath('userData'), 'database.sqlite');
// Initialise Sequelize with SQLite
export const sequelize = new Sequelize({
dialect: 'sqlite',
storage: dbPath,
dialectModule: require('sqlite3'),
logging: console.log,
define: {
timestamps: true
}
});
// ...existing code...
We also provide direct SQLite access through dbAsync
for operations where Sequelize might not be optimal:
// ...existing code...
export const dbAsync = {
run(sql: string, params: any[] = []): Promise<any> {
return new Promise((resolve, reject) => {
db.run(sql, params, function(err) {
if (err) reject(err);
else resolve({ id: this.lastID, changes: this.changes });
});
});
},
// ...other methods...
};
// ...existing code...
Asset Management
Data entries reference media assets stored in a structured file hierarchy:
/assets
│
├── /audio/ # Audio files for each song
│ ├── {songId}.mp3
│ └── {songId}.wav
│
├── /images/ # Song-related images
│ └── /{songId}/
│ ├── jacket.png
│ └── image_1.png
│
├── /shader/ # Shader-related visuals
│ ├── /background/
│ │ └── {songId}.jpg
│ └── /texture/
│ └── {songId}.jpg
│
└── /songData/ # Metadata or configuration per song
└── {songId}.json
When UI components need to access these assets, they convert relative paths from the database to absolute file paths using the Electron IPC bridge:
// ...existing code...
const [imagePath, setImagePath] = React.useState<string>('');
React.useEffect(() => {
const findImagePath = async () => {
const response = await window.electron.fileSystem.mergeAssetPath(
songDetails.jacket,
);
setImagePath(response);
};
findImagePath();
}, [songDetails.jacket]);
// ...existing code...
Workflow: Adding a YouTube Song
When a user adds a new song from YouTube, the following sequence occurs:
- User enters a YouTube URL in the
AddSongForm
- Form submits the URL to the
database:add-song
IPC channel - Main process downloads the audio using the YouTube module
- Metadata is extracted and the audio is converted to multiple formats
- A new song record is created with the YouTube ID as the unique identifier
- The song data is saved as a JSON file
- The database UI is refreshed to show the new song
Database Refresh Mechanism
Since the JSON files are the core storage location for the database, the application needs to refresh the SQLite database when:
- The application starts
- External changes are made to JSON files
- The user triggers a manual refresh
This is handled by our initialisation logic in init.ts
, which reads all JSON files and updates the database accordingly.
Sequence Diagram: Song Data Flow
Application Startup
Adding a Song
Updating a Song
Error Handling and Data Integrity
To maintain data consistency, we implement:
- JSON Backups – Every change is persisted in JSON files.
- Unique Song IDs – Ensuring data integrity.
- Validation – Data is validated before being written to storage.
- Recovery Mechanism – If SQLite fails, JSON data can be restored.
Advantages of the Hybrid Approach
- Human-Readable Data – JSON allows easy inspection and modification.
- Structured Queries – SQLite ensures efficient filtering and relationships.
- Reliability – JSON files act as persistent backups.
- Performance – SQLite enables fast queries without performance degradation.
- Cross-Platform Compatibility – Works across Windows, macOS, and Linux.
Conclusion
Our hybrid database solution balances flexibility and efficiency, combining JSON for persistent storage and SQLite for structured queries. This approach ensures data remains accessible, performant, and easy to maintain across different environments. The bidirectional synchronisation between JSON and SQLite guarantees consistency, making the system robust and scalable for future enhancements.