diff --git a/src/main/nim/wdiwtlt/db.nim b/src/main/nim/wdiwtlt/db.nim index d7bcefc..e23e22c 100644 --- a/src/main/nim/wdiwtlt/db.nim +++ b/src/main/nim/wdiwtlt/db.nim @@ -1,4 +1,4 @@ -import std/json, std/jsonutils, std/os, std/strtabs, std/tables, std/times +import std/[logging, json, jsonutils, options, os, tables, times] import timeutils, uuids, zero_functional import ./models @@ -10,7 +10,7 @@ type bookmarks: TableRef[UUID, Bookmark] mediaFiles: TableRef[UUID, MediaFile] playlists: TableRef[UUID, Playlist] - tags: StringTableRef + tags: TableRef[string, Option[string]] albumsToMediaFiles: TableRef[UUID, seq[UUID]] artistsToMediaFiles: TableRef[UUID, seq[UUID]] @@ -18,128 +18,390 @@ type playlistsToMediaFiles: TableRef[UUID, seq[UUID]] tagsAndMediaFiles: seq[tuple[tagName: string, mediaFileId: UUID]] + mediaFileHashToId: TableRef[string, UUID] + WdiwtltDb* = ref object jsonFilePath: string root: DbRoot -## API Contract +proc debug*(db: WdiwtltDb): string = + "\p\talbums: " & $db.root.albums & + "\p\tartists: " & $db.root.artists & + "\p\tbookmarks: " & $db.root.bookmarks & + "\p\tmediaFiles: " & $db.root.mediaFiles & + "\p\tplaylists: " & $db.root.playlists & + "\p\ttags: " & $db.root.tags & + "\p\talbumsToMediaFiles: " & $db.root.albumsToMediaFiles & + "\p\tartistsToMediaFiles: " & $db.root.artistsToMediaFiles & + "\p\tartistsAndAlbums: " & $db.root.artistsAndAlbums & + "\p\tplaylistsToMediaFiles: " & $db.root.playlistsToMediaFiles & + "\p\ttagsAndMediaFiles: " & $db.root.tagsAndMediaFiles & + "\p\tmediaFileHashToId: " & $db.root.mediaFileHashToId & "\p" + +## ========================================================================= ## +## API Contract ## +## ========================================================================= ## proc initDb*(path: string): WdiwtltDb; proc loadDb*(path: string): WdiwtltDb; proc persist*(db: WdiwtltDb): void; -proc delete*(db: WdiwtltDb, a: Album); -proc delete*(db: WdiwtltDb, a: Artist); -proc delete*(db: WdiwtltDb, b: Bookmark); -proc delete*(db: WdiwtltDb, mf: MediaFile); -proc delete*(db: WdiwtltDb, p: Playlist); -proc delete*(db: WdiwtltDb, t: Tag); +## Albums +## -------------------- +proc add*(db: WdiwtltDb, a: Album); +proc remove*(db: WdiwtltDb, a: Album); +proc update*(db: WdiwtltDb, a: Album); +proc findAlbumsByArtist*(db: WdiwtltDb, a: Artist): seq[Album]; +proc findAlbumsByName*(db: WdiwtltDb, name: string): seq[Album]; + +proc add*(db: WdiwtltDb, a: Album, mf: MediaFile); proc remove*(db: WdiwtltDb, a: Album, mf: MediaFile); -proc remove*(db: WdiwtltDb, a: Artist, mf: MediaFile); -proc remove*(db: WdiwtltDb, artist: Artist, album: Album); +## Artists +## -------------------- +proc add*(db: WdiwtltDb, a: Artist); +proc remove*(db: WdiwtltDb, a: Artist); +proc update*(db: WdiwtltDb, a: Artist); + +proc findArtistsByAlbum*(db: WdiwtltDb, a: Album): seq[Artist]; +proc findArtistsByName*(db: WdiwtltDb, name: string): seq[Artist]; + +proc add*(db: WdiwtltDb, a: Artist, mf: MediaFile); +proc remove*(db: WdiwtltDb, a: Artist, mf: MediaFile); + +proc isAssociated*(db: WdiwtltDb, artist: Artist, album: Album): bool; +proc associate*(db: WdiwtltDb, artist: Artist, album: Album); +proc disassociate*(db: WdiwtltDb, artist: Artist, album: Album); + +## Bookmarks +## -------------------- +proc add*(db: WdiwtltDb, b: Bookmark); +proc update*(db: WdiwtltDb, b: Bookmark); +proc remove*(db: WdiwtltDb, b: Bookmark); + +## Media Files +## -------------------- +proc add*(db: WdiwtltDb, mf: MediaFile); +proc remove*(db: WdiwtltDb, mf: MediaFile); +proc update*(db: WdiwtltDb, mf: MediaFile); +proc findMediaFilesByAlbum*(db: WdiwtltDb, a: Album): seq[MediaFile]; +proc findMediaFilesByArtist*(db: WdiwtltDb, a: Artist): seq[MediaFile]; +proc findMediaFileByHash*(db: WdiwtltDb, hash: string): Option[MediaFile]; + +## Playlists +## -------------------- +proc add*(db: WdiwtltDb, p: Playlist); +proc remove*(db: WdiwtltDb, p: Playlist); +proc update*(db: WdiwtltDb, p: Playlist); + +## Tags +## -------------------- +proc add*(db: WdiwtltDb, t: Tag); +proc remove*(db: WdiwtltDb, t: Tag); +proc update*(db: WdiwtltDb, t: Tag); + +## Housekeeping +## -------------------- proc pruneStalePlaylists*(db: WdiwtltDb, ts: DateTime); proc pruneStaleBookmarks*(db: WdiwtltDb, ts: DateTime); proc removeEmptyAlbums*(db: WdiwtltDb): void; proc removeEmptyArtists*(db: WdiwtltDb): void; proc removeEmptyPlaylists*(db: WdiwtltDb): void; +proc toJsonHook(t: TableRef[UUID, Album]): JsonNode; +proc fromJsonHook(t: var TableRef[UUID, Album], n: JsonNode); +proc toJsonHook(t: TableRef[UUID, Artist]): JsonNode; +proc fromJsonHook(t: var TableRef[UUID, Artist], n: JsonNode); +proc toJsonHook(t: TableRef[UUID, Bookmark]): JsonNode; +proc fromJsonHook(t: var TableRef[UUID, Bookmark], n: JsonNode); +proc toJsonHook(t: TableRef[UUID, MediaFile]): JsonNode; +proc fromJsonHook(t: var TableRef[UUID, MediaFile], n: JsonNode); +proc toJsonHook(t: TableRef[UUID, Playlist]): JsonNode; +proc fromJsonHook(t: var TableRef[UUID, Playlist], n: JsonNode); +proc toJsonHook(t: TableRef[UUID, seq[UUID]]): JsonNode; +proc fromJsonHook(t: var TableRef[UUID, seq[UUID]], n: JsonNode); +proc toJsonHook(uuid: UUID): JsonNode; +proc fromJsonHook(u: var UUID, node: JsonNode); +proc toJsonHook(dt: DateTime): JsonNode; +proc fromJsonHook(dt: var DateTime, node: JsonNode); + + +## ========================================================================= ## +## API Implementation ## +## ========================================================================= ## ## Internals +## -------------------- +proc toJsonHook(t: TableRef[UUID, Album]): JsonNode = + result = newJObject() + for k, v in t.pairs: result[$k] = toJson(v) + +proc fromJsonHook(t: var TableRef[UUID, Album], n: JsonNode) = + for k in n.keys: + var a: Album + fromJson(a, n[k]) + t[parseUUID(k)] = a + +proc toJsonHook(t: TableRef[UUID, Artist]): JsonNode = + result = newJObject() + for k, v in t.pairs: result[$k] = toJson(v) + +proc fromJsonHook(t: var TableRef[UUID, Artist], n: JsonNode) = + for k in n.keys: + var a: Artist + fromJson(a, n[k]) + t[parseUUID(k)] = a + +proc toJsonHook(t: TableRef[UUID, Bookmark]): JsonNode = + result = newJObject() + for k, v in t.pairs: result[$k] = toJson(v) + +proc fromJsonHook(t: var TableRef[UUID, Bookmark], n: JsonNode) = + for k in n.keys: + var b: Bookmark + fromJson(b, n[k]) + t[parseUUID(k)] = b + +proc toJsonHook(t: TableRef[UUID, MediaFile]): JsonNode = + result = newJObject() + for k, v in t.pairs: result[$k] = toJson(v) + +proc fromJsonHook(t: var TableRef[UUID, MediaFile], n: JsonNode) = + for k in n.keys: + var mf: MediaFile + fromJson(mf, n[k]) + t[parseUUID(k)] = mf + +proc toJsonHook(t: TableRef[UUID, Playlist]): JsonNode = + result = newJObject() + for k, v in t.pairs: result[$k] = toJson(v) + +proc fromJsonHook(t: var TableRef[UUID, Playlist], n: JsonNode) = + for k in n.keys: + var p: Playlist + fromJson(p, n[k]) + t[parseUUID(k)] = p + +proc toJsonHook(t: TableRef[UUID, seq[UUID]]): JsonNode = + result = newJObject() + for k, v in t.pairs: result[$k] = toJson(v) + +proc fromJsonHook(t: var TableRef[UUID, seq[UUID]], n: JsonNode) = + for k in n.keys: + var s: seq[UUID] + fromJson(s, n[k]) + t[parseUUID(k)] = s + proc toJsonHook(uuid: UUID): JsonNode = %($uuid) -proc fromJsonHook(u: var UUID,node: JsonNode) = +proc fromJsonHook(u: var UUID, node: JsonNode) = u = parseUUID(node.getStr) proc toJsonHook(dt: DateTime): JsonNode = %(dt.formatIso8601) proc fromJsonHook(dt: var DateTime, node: JsonNode) = dt = parseIso8601(node.getStr) -## API Implementation + +proc initDbRoot(): DbRoot = + DbRoot( + albums: newTable[UUID, Album](), + artists: newTable[UUID, Artist](), + bookmarks: newTable[UUID, Bookmark](), + mediaFiles: newTable[UUID, MediaFile](), + playlists: newTable[UUID, Playlist](), + tags: newTable[string, Option[string]](), + albumsToMediaFiles: newTable[UUID, seq[UUID]](), + artistsToMediaFiles: newTable[UUID, seq[UUID]](), + artistsAndAlbums: @[], + playlistsToMediaFiles: newTable[UUID, seq[UUID]](), + tagsAndMediaFiles: @[], + mediaFileHashToId: newTable[string, UUID]()) + proc initDb*(path: string): WdiwtltDb = WdiwtltDb( jsonFilePath: path, - root: DbRoot( - albums: newTable[UUID, Album](), - artists: newTable[UUID, Artist](), - bookmarks: newTable[UUID, Bookmark](), - mediaFiles: newTable[UUID, MediaFile](), - playlists: newTable[UUID, Playlist](), - tags: newStringTable(), - albumsToMediaFiles: newTable[UUID, seq[UUID]](), - artistsToMediaFiles: newTable[UUID, seq[UUID]](), - artistsAndAlbums: @[], - playlistsToMediaFiles: newTable[UUID, seq[UUID]](), - tagsAndMediaFiles: @[] - )) + root: initDbRoot()) proc loadDb*(path: string): WdiwtltDb = if not fileExists(path): raise newException(Exception, "Unable to open database file '" & path & "'") - let jsonRoot = parseJson(path) - var root: DbRoot + let jsonRoot = parseJson(path.readFile) + var root: DbRoot = initDbRoot() root.fromJson(jsonRoot) + debug "loaded DB" result = WdiwtltDb( jsonFilePath: path, root: root) + debug result.debug proc persist*(db: WdiwtltDb): void = let jsonRoot = db.root.toJson db.jsonFilePath.writeFile($jsonRoot) -proc delete*(db: WdiwtltDb, a: Album) = +## Albums +## -------------------- +proc add*(db: WdiwtltDb, a: Album) = db.root.albums[a.id] = a + +proc remove*(db: WdiwtltDb, a: Album) = db.root.albums.del(a.id) db.root.albumsToMediaFiles.del(a.id) db.root.artistsAndAlbums = db.root.artistsAndAlbums --> filter(it.albumId != a.id) -proc delete*(db: WdiwtltDb, a: Artist) = +proc update*(db: WdiwtltDb, a: Album) = db.add(a) + +proc findAlbumsByArtist*(db: WdiwtltDb, a: Artist): seq[Album] = + db.root.artistsAndAlbums --> + filter(it.artistId == a.id and db.root.artists.contains(a.id)). + map(db.root.albums[it.albumId]) + +proc findAlbumsByName*(db: WdiwtltDb, name: string): seq[Album] = + result = @[] + for a in db.root.albums.values: + if a.name == name: result.add(a) + +proc add*(db: WdiwtltDb, a: Album, mf: MediaFile) = + if not db.root.albumsToMediaFiles.contains(a.id): + db.root.albumsToMediaFiles[a.id] = @[mf.id] + else: + let idsInAlbum = db.root.albumsToMediaFiles[a.id] + if not idsInAlbum.contains(mf.id): + db.root.albumsToMediaFiles[a.id].add(mf.id) + +proc remove*(db: WdiwtltDb, a: Album, mf: MediaFile) = + if db.root.albumsToMediaFiles.contains(a.id): + db.root.albumsToMediaFiles[a.id] = + db.root.albumsToMediaFiles[a.id] --> filter(it != mf.id) + +## Artists +## -------------------- +proc add*(db: WdiwtltDb, a: Artist) = db.root.artists[a.id] = a + +proc remove*(db: WdiwtltDb, a: Artist) = db.root.artists.del(a.id) db.root.artistsToMediaFiles.del(a.id) db.root.artistsAndAlbums = db.root.artistsAndAlbums --> filter(it.artistId != a.id) -proc delete*(db: WdiwtltDb, b: Bookmark) = db.root.bookmarks.del(b.id) +proc update*(db: WdiwtltDb, a: Artist) = db.add(a) -proc delete*(db: WdiwtltDb, mf: MediaFile) = +proc findArtistsByAlbum*(db: WdiwtltDb, a: Album): seq[Artist] = + db.root.artistsAndAlbums --> + filter(it.albumId == a.id and db.root.albums.contains(a.id)). + map(db.root.artists[it.artistId]) + +proc findArtistsByName*(db: WdiwtltDb, name: string): seq[Artist] = + result = @[] + for a in db.root.artists.values: + if a.name == name: result.add(a) + +proc add*(db: WdiwtltDb, a: Artist, mf: MediaFile) = + if not db.root.artistsToMediaFiles.contains(a.id): + db.root.artistsToMediaFiles[a.id] = @[mf.id] + else: + let idsInArtist = db.root.artistsToMediaFiles[a.id] + if not idsInArtist.contains(mf.id): + db.root.artistsToMediaFiles[a.id].add(mf.id) + +proc remove*(db: WdiwtltDb, a: Artist, mf: MediaFile) = + if db.root.artistsToMediaFiles.contains(a.id): + db.root.artistsToMediaFiles[a.id] = + db.root.artistsToMediaFiles[a.id] --> filter(it != mf.id) + +proc isAssociated*( + db: WdiwtltDb, + artist: Artist, + album: Album + ): bool = + let matching = db.root.artistsAndAlbums --> + filter(it.artistId == artist.id and it.albumId == album.id) + return matching.len > 0 + +proc associate*(db: WdiwtltDb, artist: Artist, album: Album) = + if not db.isAssociated(artist, album): + db.root.artistsAndAlbums.add((artist.id, album.id)) + +proc disassociate*(db: WdiwtltDb, artist: Artist, album: Album) = + db.root.artistsAndAlbums = db.root.artistsAndAlbums --> + filter(it.albumId != album.id or it.artistId != artist.id) + +## Bookmarks +## -------------------- +proc add*(db: WdiwtltDb, b: Bookmark) = db.root.bookmarks[b.id] = b +proc remove*(db: WdiwtltDb, b: Bookmark) = db.root.bookmarks.del(b.id) +proc update*(db: WdiwtltDb, b: Bookmark) = db.add(b) + +## Media Files +## -------------------- +proc add*(db: WdiwtltDb, mf: MediaFile) = + db.root.mediaFiles[mf.id] = mf + db.root.mediaFileHashToId[mf.fileHash] = mf.id + +proc remove*(db: WdiwtltDb, mf: MediaFile) = db.root.mediaFiles.del(mf.id) for albumId, mfIds in db.root.albumsToMediaFiles.pairs: if mfIds.contains(mf.id): db.remove(db.root.albums[albumId], mf) -proc delete*(db: WdiwtltDb, p: Playlist) = + db.root.mediaFileHashToId.del(mf.fileHash) + +proc update*(db: WdiwtltDb, mf: MediaFile) = db.add(mf) + +proc findMediaFilesByAlbum*(db: WdiwtltDb, a: Album): seq[MediaFile] = + result = @[] + if db.root.albumsToMediaFiles.contains(a.id): + return db.root.albumsToMediaFiles[a.id] --> + filter(db.root.mediaFiles.contains(it)). + map(db.root.mediaFiles[it]) + +proc findMediaFilesByArtist*(db: WdiwtltDb, a: Artist): seq[MediaFile] = + result = @[] + if db.root.artistsToMediaFiles.contains(a.id): + return db.root.artistsToMediaFiles[a.id] --> + filter(db.root.mediaFiles.contains(it)). + map(db.root.mediaFiles[it]) + +proc findMediaFileByHash*(db: WdiwtltDb, hash: string): Option[MediaFile] = + if db.root.mediaFileHashToId.contains(hash): + let mfId = db.root.mediaFileHashToId[hash] + if db.root.mediaFiles.contains(mfId): + return some(db.root.mediaFiles[mfId]) + + return none[MediaFile]() + +## Playlists +## -------------------- +proc add*(db: WdiwtltDb, p: Playlist) = db.root.playlists[p.id] = p + +proc remove*(db: WdiwtltDb, p: Playlist) = db.root.playlists.del(p.id) db.root.playlistsToMediaFiles.del(p.id) for b in db.root.bookmarks.values: - if b.playlistId == p.id: db.delete(b) + if b.playlistId == p.id: db.remove(b) -proc delete*(db: WdiwtltDb, t: Tag) = +proc update*(db: WdiwtltDb, p: Playlist) = db.add(p) + +## Tags +## -------------------- +proc add*(db: WdiwtltDb, t: Tag) = db.root.tags[t.name] = t.description + +proc remove*(db: WdiwtltDb, t: Tag) = db.root.tags.del(t.name) db.root.tagsAndMediaFiles = db.root.tagsAndMediaFiles --> filter(it.tagName != t.name) -proc remove*(db: WdiwtltDb, a: Album, mf: MediaFile) = - db.root.albumsToMediaFiles[a.id] = - db.root.albumsToMediaFiles[a.id] --> - filter(it != mf.id) - -proc remove*(db: WdiwtltDb, a: Artist, mf: MediaFile) = - db.root.artistsToMediaFiles[a.id] = - db.root.artistsToMediaFiles[a.id] --> - filter(it != mf.id) - -proc remove*(db: WdiwtltDb, artist: Artist, album: Album) = - db.root.artistsAndAlbums = db.root.artistsAndAlbums --> - filter(it.albumId != album.id or it.artistId != artist.id) +proc update*(db: WdiwtltDb, t: Tag) = db.add(t) +## Housekeeping +## -------------------- proc pruneStalePlaylists*(db: WdiwtltDb, ts: DateTime) = db.root.playlists.values --> - filter(it.lastUsed > ts).foreach(db.delete(it)) + filter(it.lastUsed > ts).foreach(db.remove(it)) proc pruneStaleBookmarks*(db: WdiwtltDb, ts: DateTime) = db.root.bookmarks.values --> - filter(it.lastUsed > ts).foreach(db.delete(it)) + filter(it.lastUsed > ts).foreach(db.remove(it)) proc removeEmptyAlbums*(db: WdiwtltDb): void = let emptyAlbumIds = db.root.albumsToMediaFiles.pairs -->