WIP. A little custom music server
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

move shit around

+97 -106
+1 -1
backend/src/api.ts
··· 2 2 import { Elysia, status, StatusMap, t, type HTTPHeaders } from "elysia"; 3 3 import { DatabaseLive } from "./db"; 4 4 import { BunContext } from "@effect/platform-bun"; 5 - import { EnvLive } from "./env"; 5 + import { EnvLive } from "./utils/env"; 6 6 import { albumTable, artistTable, fileTable, songTable, songToArtistTable } from "./db/schema"; 7 7 import { eq } from "drizzle-orm"; 8 8 import { openapi } from "@elysiajs/openapi";
backend/src/benchmark-stream.ts backend/src/benchmarking/benchmark-stream.ts
backend/src/benchmark.ts backend/src/benchmarking/benchmark.ts
backend/src/chalk.ts backend/src/utils/chalk.ts
backend/src/config.ts backend/src/utils/config.ts
+1 -1
backend/src/db/migrate.ts
··· 3 3 import { drizzle } from "drizzle-orm/bun-sqlite"; 4 4 import { migrate } from "drizzle-orm/bun-sqlite/migrator"; 5 5 import { Database } from "bun:sqlite"; 6 - import { Env, EnvLive } from "../env"; 6 + import { Env, EnvLive } from "../utils/env"; 7 7 8 8 const main = Effect.gen(function* () { 9 9 const env = yield* Env.pipe(Effect.flatMap((x) => x.getEnv));
backend/src/env.ts backend/src/utils/env.ts
+1 -1
backend/src/flac.ts
··· 2 2 import { Console, Data, Effect, Option, ParseResult, Schema } from "effect"; 3 3 4 4 import { type Metadata, MetadataSchema, MetadataWithFilepathSchema } from "./metadata"; 5 - import { Bit, Int24, safeParseInt, Uint7 } from "./utils"; 5 + import { Bit, Int24, safeParseInt, Uint7 } from "./utils/utils"; 6 6 7 7 const VORBIS_STREAMINFO = 4; 8 8
+79 -88
backend/src/flac/transformers.ts
··· 1 1 import { Effect, Option, ParseResult, Schema } from "effect"; 2 2 3 3 import { type Metadata, MetadataSchema } from "~/metadata"; 4 - import { Bit, Int24, safeParseInt, Uint7 } from "~/utils"; 4 + import { Bit, Int24, safeParseInt, Uint7 } from "~/utils/utils"; 5 5 6 6 const FlacHeader = Schema.Struct({ 7 7 isLast: Bit, // 1 bit ··· 14 14 offset: Schema.Number, 15 15 }); 16 16 17 - export const FlacHeaderFromUint8Array = Schema.transformOrFail( 18 - FlacHeaderInput, 19 - FlacHeader, 20 - { 21 - strict: true, 22 - decode({ uint8Array, offset }, _, ast) { 23 - return Effect.gen(function* () { 24 - if (uint8Array.length < 4) { 25 - return yield* ParseResult.fail(new ParseResult.Type(ast, uint8Array, "UIntArray is too short")); 26 - } 17 + export const FlacHeaderFromUint8Array = Schema.transformOrFail(FlacHeaderInput, FlacHeader, { 18 + strict: true, 19 + decode({ uint8Array, offset }, _, ast) { 20 + return Effect.gen(function* () { 21 + if (uint8Array.length < 4) { 22 + return yield* ParseResult.fail(new ParseResult.Type(ast, uint8Array, "UIntArray is too short")); 23 + } 27 24 28 - const dataView = new DataView(uint8Array.buffer, offset, 4); 25 + const dataView = new DataView(uint8Array.buffer, offset, 4); 29 26 30 - const header = { 31 - isLast: ((dataView.getUint8(0) & 0x80) === 0x80 ? 1 : 0) as Bit, // 1 bit 32 - streamInfo: dataView.getUint8(0) & 0x7f, // 7 bits 33 - length: dataView.getUint32(0, false) & 0xffffff, // 3 byte 34 - }; 27 + const header = { 28 + isLast: ((dataView.getUint8(0) & 0x80) === 0x80 ? 1 : 0) as Bit, // 1 bit 29 + streamInfo: dataView.getUint8(0) & 0x7f, // 7 bits 30 + length: dataView.getUint32(0, false) & 0xffffff, // 3 byte 31 + }; 35 32 36 - return yield* ParseResult.succeed(header); 37 - }); 38 - }, 39 - encode(x, _, ast) { 40 - return ParseResult.fail(new ParseResult.Type(ast, x, "Unimplemented")); 41 - }, 33 + return yield* ParseResult.succeed(header); 34 + }); 42 35 }, 43 - ); 44 - 36 + encode(x, _, ast) { 37 + return ParseResult.fail(new ParseResult.Type(ast, x, "Unimplemented")); 38 + }, 39 + }); 45 40 46 41 const MetadataInput = Schema.Struct({ 47 42 uint8Array: Schema.Uint8ArrayFromSelf, ··· 49 44 length: Schema.Number, 50 45 }); 51 46 52 - export const MetadataFromUint8Array = Schema.transformOrFail( 53 - MetadataInput, 54 - MetadataSchema, 55 - { 56 - strict: true, 57 - decode({ uint8Array, offset, length }, _, ast) { 58 - return Effect.gen(function* () { 59 - const dv = new DataView(uint8Array.buffer, offset, length); 47 + export const MetadataFromUint8Array = Schema.transformOrFail(MetadataInput, MetadataSchema, { 48 + strict: true, 49 + decode({ uint8Array, offset, length }, _, ast) { 50 + return Effect.gen(function* () { 51 + const dv = new DataView(uint8Array.buffer, offset, length); 60 52 61 - let cursor = 0; 53 + let cursor = 0; 62 54 63 - const vendorStringLength = dv.getUint32(cursor, true); 64 - cursor += 4; 55 + const vendorStringLength = dv.getUint32(cursor, true); 56 + cursor += 4; 57 + 58 + cursor += vendorStringLength; 59 + 60 + const numberOfFields = dv.getUint32(cursor, true); 61 + cursor += 4; 65 62 66 - cursor += vendorStringLength; 63 + const metadata: Partial<Metadata & { artists: string[] }> = {}; 67 64 68 - const numberOfFields = dv.getUint32(cursor, true); 65 + for (let i = 0; i < numberOfFields; i++) { 66 + const fieldLength = dv.getUint32(cursor, true); 69 67 cursor += 4; 70 68 71 - const metadata: Partial<Metadata & { artists: string[] }> = {}; 69 + const fieldValue = uint8Array.slice(cursor, cursor + fieldLength).toString(); 70 + cursor += fieldLength; 72 71 73 - for (let i = 0; i < numberOfFields; i++) { 74 - const fieldLength = dv.getUint32(cursor, true); 75 - cursor += 4; 72 + const parsedField = parseFieldValue(fieldValue); 76 73 77 - const fieldValue = uint8Array.slice(cursor, cursor + fieldLength).toString(); 78 - cursor += fieldLength; 74 + if (!parsedField) { 75 + continue; 76 + } 79 77 80 - const parsedField = parseFieldValue(fieldValue); 78 + const { key, value } = parsedField; 81 79 82 - if (!parsedField) { 80 + switch (key.toUpperCase()) { 81 + case "ALBUM": 82 + metadata.album = value; 83 + break; 84 + case "ARTIST": 85 + if (!metadata.artists) { 86 + metadata.artists = []; 87 + } 88 + metadata.artists.push(value); 89 + break; 90 + case "ALBUM ARTIST": 91 + metadata.albumArtist = value; 92 + break; 93 + case "TITLE": 94 + metadata.title = value; 95 + break; 96 + case "TRACKNUMBER": 97 + const parsedNumber = yield* safeParseInt(value, 10); 98 + if (Option.isSome(parsedNumber)) { 99 + metadata.trackNumber = parsedNumber.value; 100 + } 101 + break; 102 + default: 83 103 continue; 84 - } 85 - 86 - const { key, value } = parsedField; 87 - 88 - switch (key.toUpperCase()) { 89 - case "ALBUM": 90 - metadata.album = value; 91 - break; 92 - case "ARTIST": 93 - if (!metadata.artists) { 94 - metadata.artists = []; 95 - } 96 - metadata.artists.push(value); 97 - break; 98 - case "ALBUM ARTIST": 99 - metadata.albumArtist = value; 100 - break; 101 - case "TITLE": 102 - metadata.title = value; 103 - break; 104 - case "TRACKNUMBER": 105 - const parsedNumber = yield* safeParseInt(value, 10); 106 - if (Option.isSome(parsedNumber)) { 107 - metadata.trackNumber = parsedNumber.value; 108 - } 109 - break; 110 - default: 111 - continue; 112 - } 113 104 } 105 + } 114 106 115 - metadata.albumArtist ??= metadata.artists?.at(0); 107 + metadata.albumArtist ??= metadata.artists?.at(0); 116 108 117 - const valid = Schema.decodeUnknownOption(MetadataSchema)(metadata); 109 + const valid = Schema.decodeUnknownOption(MetadataSchema)(metadata); 118 110 119 - return yield* Option.match(valid, { 120 - onSome: (value) => ParseResult.succeed(value), 121 - onNone: () => ParseResult.fail(new ParseResult.Type(ast, metadata, "Metadata is not valid")), 122 - }); 111 + return yield* Option.match(valid, { 112 + onSome: (value) => ParseResult.succeed(value), 113 + onNone: () => ParseResult.fail(new ParseResult.Type(ast, metadata, "Metadata is not valid")), 123 114 }); 124 - }, 125 - encode(x, _, ast) { 126 - return ParseResult.fail(new ParseResult.Type(ast, x, "Unimplemented")); 127 - }, 115 + }); 128 116 }, 129 - ); 130 - 117 + encode(x, _, ast) { 118 + return ParseResult.fail(new ParseResult.Type(ast, x, "Unimplemented")); 119 + }, 120 + }); 131 121 132 122 function parseFieldValue(str: string): { key: string; value: string } | null { 133 123 const [key, value] = str.split("=").map((x) => x.trim()); ··· 135 125 return null; 136 126 } 137 127 return { key: key.toUpperCase(), value }; 138 - } 128 + } 129 +
+1 -1
backend/src/index.ts
··· 2 2 import { Config, Cron, Effect, Either, Layer, Option, Schedule } from "effect"; 3 3 import { DatabaseLive } from "./db"; 4 4 import { syncLibraryStream } from "./sync-library"; 5 - import { OtelLive } from "./otel"; 5 + import { OtelLive } from "./utils/otel"; 6 6 import { startApi } from "./api"; 7 7 import { FlacService } from "./flac/service"; 8 8
backend/src/otel.ts backend/src/utils/otel.ts
+14 -14
backend/src/sync-library.ts
··· 4 4 5 5 import { albumTable, artistTable, artistToAlbumTable, fileTable, songTable, songToArtistTable } from "./db/schema"; 6 6 import type { MetadataWithFilepathSchema } from "./metadata"; 7 - import { colors } from "./chalk"; 7 + import { colors } from "./utils/chalk"; 8 8 9 9 type Metadata = typeof MetadataWithFilepathSchema.Type; 10 10 ··· 16 16 17 17 const stream = yield* readDirectory( 18 18 libraryPath, 19 - alreadyIndexed.map((x) => x.path) 19 + alreadyIndexed.map((x) => x.path), 20 20 ); 21 21 22 22 yield* stream.pipe( ··· 29 29 "by", 30 30 colors.FgYellow, 31 31 x.artists.join(","), 32 - colors.Reset 33 - ) 32 + colors.Reset, 33 + ), 34 34 ), 35 35 Stream.grouped(10), 36 36 //Stream.schedule(Schedule.spaced("3 second")), 37 37 Stream.mapEffect(saveChunk, { concurrency: 5 }), 38 - Stream.runDrain 38 + Stream.runDrain, 39 39 ); 40 40 }); 41 41 42 42 const saveChunk = Effect.fn("save-chunk")(function* (chunk: Chunk.Chunk<Metadata>) { 43 43 const [files, artists, albums] = yield* Effect.all( 44 44 [createFiles(chunk), createArtists(chunk), createAlbums(chunk)], 45 - { concurrency: 3 } 45 + { concurrency: 3 }, 46 46 ); 47 47 48 48 const songs = yield* createSongs(chunk, { files, albums }); ··· 58 58 albums, 59 59 }), 60 60 ], 61 - { concurrency: 2 } 61 + { concurrency: 2 }, 62 62 ); 63 63 64 64 return { ··· 74 74 lookup: { 75 75 songs: Effect.Effect.Success<ReturnType<typeof createSongs>>; 76 76 artists: Effect.Effect.Success<ReturnType<typeof createArtists>>; 77 - } 77 + }, 78 78 ) { 79 79 const db = yield* DatabaseLive; 80 80 const newSongArtist = Chunk.toArray(chunk) ··· 82 82 entry.artists.map((artist) => ({ 83 83 artistId: lookup.artists.find((x) => x.name === artist)?.id ?? "", 84 84 songId: lookup.songs.find((x) => x.title === entry.title)?.id ?? "", 85 - })) 85 + })), 86 86 ) 87 87 .filter((x) => x.songId && x.artistId); 88 88 ··· 99 99 lookup: { 100 100 albums: Effect.Effect.Success<ReturnType<typeof createAlbums>>; 101 101 artists: Effect.Effect.Success<ReturnType<typeof createArtists>>; 102 - } 102 + }, 103 103 ) { 104 104 const db = yield* DatabaseLive; 105 105 const newAlbumArtists = Chunk.toArray(chunk) ··· 121 121 lookup: { 122 122 files: Effect.Effect.Success<ReturnType<typeof createFiles>>; 123 123 albums: Effect.Effect.Success<ReturnType<typeof createAlbums>>; 124 - } 124 + }, 125 125 ) { 126 126 const db = yield* DatabaseLive; 127 127 ··· 180 180 .values( 181 181 newArtists.map((x) => ({ 182 182 name: x, 183 - })) 183 + })), 184 184 ) 185 185 .onConflictDoNothing() 186 186 .returning(); ··· 207 207 .values( 208 208 newAlbums.map((x) => ({ 209 209 title: x, 210 - })) 210 + })), 211 211 ) 212 212 .onConflictDoNothing() 213 213 .returning(); ··· 222 222 223 223 function chunkToUniqueArray<T extends object, U extends string | readonly string[]>( 224 224 chunk: Chunk.Chunk<T>, 225 - map: (x: T) => U 225 + map: (x: T) => U, 226 226 ) { 227 227 const array = Chunk.toArray(chunk); 228 228 const x = array.flatMap(map);
backend/src/utils.ts backend/src/utils/utils.ts