diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index fe09204..311417d 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -1600,6 +1600,12 @@ dependencies = [
"pin-project-lite",
]
+[[package]]
+name = "http-range"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
+
[[package]]
name = "httparse"
version = "1.10.1"
@@ -3987,6 +3993,7 @@ dependencies = [
"gtk",
"heck 0.5.0",
"http",
+ "http-range",
"jni",
"libc",
"log",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 9ddbec8..ecc5d79 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -18,7 +18,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
tauri-build = { version = "2", features = [] }
[dependencies]
-tauri = { version = "2", features = [] }
+tauri = { version = "2", features = ["protocol-asset"] }
tauri-plugin-opener = "2"
tauri-plugin-store = "2"
tauri-plugin-dialog = "2"
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index e008cea..f481176 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -19,7 +19,11 @@
}
],
"security": {
- "csp": null
+ "csp": "default-src 'self' ipc: http://ipc.localhost; img-src 'self' asset: http://asset.localhost; style-src 'self' 'unsafe-inline'",
+ "assetProtocol": {
+ "enable": true,
+ "scope": ["**"]
+ }
}
},
"bundle": {
diff --git a/src/lib/components/TrackList.svelte b/src/lib/components/TrackList.svelte
new file mode 100644
index 0000000..d7a3331
--- /dev/null
+++ b/src/lib/components/TrackList.svelte
@@ -0,0 +1,80 @@
+
+
+
+
+
+
+ | # |
+ Title |
+ Artist |
+ Album |
+ Duration |
+
+
+
+ {#each tracks as track, i}
+ handleRowClick(i)}
+ >
+ | {track.metadata.trackNumber ?? i + 1} |
+ {track.metadata.title ?? track.filename} |
+ {track.metadata.artist ?? 'Unknown Artist'} |
+ {track.metadata.album ?? 'Unknown Album'} |
+ {formatDuration(track.metadata.duration)} |
+
+ {/each}
+
+
+
+
+
diff --git a/src/lib/library/scanner.ts b/src/lib/library/scanner.ts
index 0522de3..461fc06 100644
--- a/src/lib/library/scanner.ts
+++ b/src/lib/library/scanner.ts
@@ -1,4 +1,6 @@
-import { readDir } from '@tauri-apps/plugin-fs';
+import { readDir, readFile } from '@tauri-apps/plugin-fs';
+import { parseBuffer } from 'music-metadata';
+import type { Album, ArtistWithAlbums, AudioFormat } from '$lib/types/track';
export interface Artist {
name: string;
@@ -38,6 +40,236 @@ export async function scanArtists(musicFolderPath: string): Promise {
}
}
+/**
+ * Find cover art image in album directory
+ */
+async function findAlbumArt(albumPath: string): Promise {
+ try {
+ const entries = await readDir(albumPath);
+ const imageExtensions = ['.jpg', '.jpeg', '.png', '.JPG', '.JPEG', '.PNG'];
+
+ for (const entry of entries) {
+ if (!entry.isDirectory) {
+ const hasImageExt = imageExtensions.some(ext => entry.name.endsWith(ext));
+ if (hasImageExt) {
+ return `${albumPath}/${entry.name}`;
+ }
+ }
+ }
+ return undefined;
+ } catch (error) {
+ console.error('Error finding album art:', error);
+ return undefined;
+ }
+}
+
+/**
+ * Get audio format from file extension
+ */
+function getAudioFormat(filename: string): AudioFormat {
+ const ext = filename.toLowerCase().split('.').pop();
+ switch (ext) {
+ case 'flac':
+ return 'flac';
+ case 'mp3':
+ return 'mp3';
+ case 'opus':
+ return 'opus';
+ case 'ogg':
+ return 'ogg';
+ case 'm4a':
+ return 'm4a';
+ case 'wav':
+ return 'wav';
+ default:
+ return 'unknown';
+ }
+}
+
+/**
+ * Read year from first audio file in album directory
+ */
+async function getAlbumYear(albumPath: string): Promise {
+ try {
+ const entries = await readDir(albumPath);
+ const audioExtensions = ['.flac', '.mp3', '.opus', '.ogg', '.m4a', '.wav', '.FLAC', '.MP3'];
+
+ // Find first audio file
+ for (const entry of entries) {
+ if (!entry.isDirectory) {
+ const hasAudioExt = audioExtensions.some(ext => entry.name.endsWith(ext));
+ if (hasAudioExt) {
+ const filePath = `${albumPath}/${entry.name}`;
+ const format = getAudioFormat(entry.name);
+
+ // Read file and parse metadata
+ const fileData = await readFile(filePath);
+ const mimeMap: Record = {
+ 'flac': 'audio/flac',
+ 'mp3': 'audio/mpeg',
+ 'opus': 'audio/opus',
+ 'ogg': 'audio/ogg',
+ 'm4a': 'audio/mp4',
+ 'wav': 'audio/wav',
+ 'unknown': 'audio/mpeg'
+ };
+
+ const metadata = await parseBuffer(fileData, mimeMap[format]);
+ return metadata.common.year;
+ }
+ }
+ }
+ return undefined;
+ } catch (error) {
+ console.error('Error reading album year:', error);
+ return undefined;
+ }
+}
+
+/**
+ * Count audio files in album directory
+ */
+async function countTracks(albumPath: string): Promise {
+ try {
+ const entries = await readDir(albumPath);
+ const audioExtensions = ['.flac', '.mp3', '.opus', '.ogg', '.m4a', '.wav', '.FLAC', '.MP3'];
+
+ let count = 0;
+ for (const entry of entries) {
+ if (!entry.isDirectory) {
+ const hasAudioExt = audioExtensions.some(ext => entry.name.endsWith(ext));
+ if (hasAudioExt) {
+ count++;
+ }
+ }
+ }
+ return count;
+ } catch (error) {
+ console.error('Error counting tracks:', error);
+ return 0;
+ }
+}
+
+/**
+ * Scan all albums in music folder
+ */
+export async function scanAlbums(musicFolderPath: string): Promise {
+ try {
+ const artistEntries = await readDir(musicFolderPath);
+ const albums: Album[] = [];
+
+ for (const artistEntry of artistEntries) {
+ // Skip _temp folder and non-directories
+ if (!artistEntry.isDirectory || artistEntry.name === '_temp') {
+ continue;
+ }
+
+ const artistPath = `${musicFolderPath}/${artistEntry.name}`;
+ const albumEntries = await readDir(artistPath);
+
+ for (const albumEntry of albumEntries) {
+ if (albumEntry.isDirectory) {
+ const albumPath = `${artistPath}/${albumEntry.name}`;
+ const trackCount = await countTracks(albumPath);
+
+ // Only include if there are tracks
+ if (trackCount > 0) {
+ const [coverArtPath, year] = await Promise.all([
+ findAlbumArt(albumPath),
+ getAlbumYear(albumPath)
+ ]);
+
+ albums.push({
+ artist: artistEntry.name,
+ title: albumEntry.name,
+ path: albumPath,
+ coverArtPath,
+ trackCount,
+ year
+ });
+ }
+ }
+ }
+ }
+
+ // Sort by artist then album title
+ albums.sort((a, b) => {
+ const artistCompare = a.artist.localeCompare(b.artist);
+ return artistCompare !== 0 ? artistCompare : a.title.localeCompare(b.title);
+ });
+
+ return albums;
+ } catch (error) {
+ console.error('Error scanning albums:', error);
+ return [];
+ }
+}
+
+/**
+ * Scan artists with their albums
+ */
+export async function scanArtistsWithAlbums(musicFolderPath: string): Promise {
+ try {
+ const artistEntries = await readDir(musicFolderPath);
+ const artists: ArtistWithAlbums[] = [];
+
+ for (const artistEntry of artistEntries) {
+ // Skip _temp folder and non-directories
+ if (!artistEntry.isDirectory || artistEntry.name === '_temp') {
+ continue;
+ }
+
+ const artistPath = `${musicFolderPath}/${artistEntry.name}`;
+ const albumEntries = await readDir(artistPath);
+ const albums: Album[] = [];
+
+ for (const albumEntry of albumEntries) {
+ if (albumEntry.isDirectory) {
+ const albumPath = `${artistPath}/${albumEntry.name}`;
+ const trackCount = await countTracks(albumPath);
+
+ // Only include if there are tracks
+ if (trackCount > 0) {
+ const [coverArtPath, year] = await Promise.all([
+ findAlbumArt(albumPath),
+ getAlbumYear(albumPath)
+ ]);
+
+ albums.push({
+ artist: artistEntry.name,
+ title: albumEntry.name,
+ path: albumPath,
+ coverArtPath,
+ trackCount,
+ year
+ });
+ }
+ }
+ }
+
+ // Only add artist if they have albums
+ if (albums.length > 0) {
+ albums.sort((a, b) => a.title.localeCompare(b.title));
+
+ artists.push({
+ name: artistEntry.name,
+ path: artistPath,
+ albums,
+ primaryCoverArt: albums[0]?.coverArtPath
+ });
+ }
+ }
+
+ // Sort alphabetically by artist name
+ artists.sort((a, b) => a.name.localeCompare(b.name));
+
+ return artists;
+ } catch (error) {
+ console.error('Error scanning artists with albums:', error);
+ return [];
+ }
+}
+
/**
* Scan playlists folder for m3u/m3u8 files
*/
diff --git a/src/lib/types/track.ts b/src/lib/types/track.ts
index f362452..0038b29 100644
--- a/src/lib/types/track.ts
+++ b/src/lib/types/track.ts
@@ -29,6 +29,28 @@ export interface Track {
metadata: TrackMetadata;
}
+/**
+ * Album information
+ */
+export interface Album {
+ artist: string; // Album artist
+ title: string; // Album title
+ year?: number;
+ path: string; // Full path to album directory
+ coverArtPath?: string; // Path to cover art image if found
+ trackCount: number;
+}
+
+/**
+ * Artist with their albums
+ */
+export interface ArtistWithAlbums {
+ name: string;
+ path: string;
+ albums: Album[];
+ primaryCoverArt?: string; // Cover art from first album for thumbnail
+}
+
/**
* Playlist with tracks
*/
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 2342d1c..390e38b 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -180,7 +180,7 @@
flex: 1;
min-width: 0;
overflow-y: auto;
- padding: 8px;
+ padding: 0;
font-family: "Pixelated MS Sans Serif", Arial;
background: #121212;
}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 3d8cd30..e5dca62 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -2,7 +2,7 @@
import { settings } from '$lib/stores/settings';
-
+
Welcome to Shark!
Your music library manager and player.
diff --git a/src/routes/downloads/+page.svelte b/src/routes/downloads/+page.svelte
index 4733799..296c661 100644
--- a/src/routes/downloads/+page.svelte
+++ b/src/routes/downloads/+page.svelte
@@ -108,9 +108,9 @@
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte
index 0a8122d..3ea27f5 100644
--- a/src/routes/settings/+page.svelte
+++ b/src/routes/settings/+page.svelte
@@ -65,7 +65,7 @@
}
-