WIP: tempoaray DB implementation.

This commit is contained in:
Jonathan Bernard 2022-10-18 21:50:20 -05:00
parent 87202437a8
commit 8788015c4f

View File

@ -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 timeutils, uuids, zero_functional
import ./models import ./models
@ -10,7 +10,7 @@ type
bookmarks: TableRef[UUID, Bookmark] bookmarks: TableRef[UUID, Bookmark]
mediaFiles: TableRef[UUID, MediaFile] mediaFiles: TableRef[UUID, MediaFile]
playlists: TableRef[UUID, Playlist] playlists: TableRef[UUID, Playlist]
tags: StringTableRef tags: TableRef[string, Option[string]]
albumsToMediaFiles: TableRef[UUID, seq[UUID]] albumsToMediaFiles: TableRef[UUID, seq[UUID]]
artistsToMediaFiles: TableRef[UUID, seq[UUID]] artistsToMediaFiles: TableRef[UUID, seq[UUID]]
@ -18,128 +18,390 @@ type
playlistsToMediaFiles: TableRef[UUID, seq[UUID]] playlistsToMediaFiles: TableRef[UUID, seq[UUID]]
tagsAndMediaFiles: seq[tuple[tagName: string, mediaFileId: UUID]] tagsAndMediaFiles: seq[tuple[tagName: string, mediaFileId: UUID]]
mediaFileHashToId: TableRef[string, UUID]
WdiwtltDb* = ref object WdiwtltDb* = ref object
jsonFilePath: string jsonFilePath: string
root: DbRoot 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 initDb*(path: string): WdiwtltDb;
proc loadDb*(path: string): WdiwtltDb; proc loadDb*(path: string): WdiwtltDb;
proc persist*(db: WdiwtltDb): void; proc persist*(db: WdiwtltDb): void;
proc delete*(db: WdiwtltDb, a: Album); ## Albums
proc delete*(db: WdiwtltDb, a: Artist); ## --------------------
proc delete*(db: WdiwtltDb, b: Bookmark); proc add*(db: WdiwtltDb, a: Album);
proc delete*(db: WdiwtltDb, mf: MediaFile); proc remove*(db: WdiwtltDb, a: Album);
proc delete*(db: WdiwtltDb, p: Playlist); proc update*(db: WdiwtltDb, a: Album);
proc delete*(db: WdiwtltDb, t: Tag);
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: 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 pruneStalePlaylists*(db: WdiwtltDb, ts: DateTime);
proc pruneStaleBookmarks*(db: WdiwtltDb, ts: DateTime); proc pruneStaleBookmarks*(db: WdiwtltDb, ts: DateTime);
proc removeEmptyAlbums*(db: WdiwtltDb): void; proc removeEmptyAlbums*(db: WdiwtltDb): void;
proc removeEmptyArtists*(db: WdiwtltDb): void; proc removeEmptyArtists*(db: WdiwtltDb): void;
proc removeEmptyPlaylists*(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 ## 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 toJsonHook(uuid: UUID): JsonNode = %($uuid)
proc fromJsonHook(u: var UUID,node: JsonNode) = proc fromJsonHook(u: var UUID, node: JsonNode) =
u = parseUUID(node.getStr) u = parseUUID(node.getStr)
proc toJsonHook(dt: DateTime): JsonNode = %(dt.formatIso8601) proc toJsonHook(dt: DateTime): JsonNode = %(dt.formatIso8601)
proc fromJsonHook(dt: var DateTime, node: JsonNode) = proc fromJsonHook(dt: var DateTime, node: JsonNode) =
dt = parseIso8601(node.getStr) 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 = proc initDb*(path: string): WdiwtltDb =
WdiwtltDb( WdiwtltDb(
jsonFilePath: path, jsonFilePath: path,
root: DbRoot( root: initDbRoot())
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: @[]
))
proc loadDb*(path: string): WdiwtltDb = proc loadDb*(path: string): WdiwtltDb =
if not fileExists(path): if not fileExists(path):
raise newException(Exception, "Unable to open database file '" & path & "'") raise newException(Exception, "Unable to open database file '" & path & "'")
let jsonRoot = parseJson(path) let jsonRoot = parseJson(path.readFile)
var root: DbRoot var root: DbRoot = initDbRoot()
root.fromJson(jsonRoot) root.fromJson(jsonRoot)
debug "loaded DB"
result = WdiwtltDb( result = WdiwtltDb(
jsonFilePath: path, jsonFilePath: path,
root: root) root: root)
debug result.debug
proc persist*(db: WdiwtltDb): void = proc persist*(db: WdiwtltDb): void =
let jsonRoot = db.root.toJson let jsonRoot = db.root.toJson
db.jsonFilePath.writeFile($jsonRoot) 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.albums.del(a.id)
db.root.albumsToMediaFiles.del(a.id) db.root.albumsToMediaFiles.del(a.id)
db.root.artistsAndAlbums = db.root.artistsAndAlbums =
db.root.artistsAndAlbums --> filter(it.albumId != a.id) 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.artists.del(a.id)
db.root.artistsToMediaFiles.del(a.id) db.root.artistsToMediaFiles.del(a.id)
db.root.artistsAndAlbums = db.root.artistsAndAlbums =
db.root.artistsAndAlbums --> filter(it.artistId != a.id) 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) db.root.mediaFiles.del(mf.id)
for albumId, mfIds in db.root.albumsToMediaFiles.pairs: for albumId, mfIds in db.root.albumsToMediaFiles.pairs:
if mfIds.contains(mf.id): if mfIds.contains(mf.id):
db.remove(db.root.albums[albumId], mf) 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.playlists.del(p.id)
db.root.playlistsToMediaFiles.del(p.id) db.root.playlistsToMediaFiles.del(p.id)
for b in db.root.bookmarks.values: 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.tags.del(t.name)
db.root.tagsAndMediaFiles = db.root.tagsAndMediaFiles --> db.root.tagsAndMediaFiles = db.root.tagsAndMediaFiles -->
filter(it.tagName != t.name) filter(it.tagName != t.name)
proc remove*(db: WdiwtltDb, a: Album, mf: MediaFile) = proc update*(db: WdiwtltDb, t: Tag) = db.add(t)
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)
## Housekeeping
## --------------------
proc pruneStalePlaylists*(db: WdiwtltDb, ts: DateTime) = proc pruneStalePlaylists*(db: WdiwtltDb, ts: DateTime) =
db.root.playlists.values --> 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) = proc pruneStaleBookmarks*(db: WdiwtltDb, ts: DateTime) =
db.root.bookmarks.values --> 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 = proc removeEmptyAlbums*(db: WdiwtltDb): void =
let emptyAlbumIds = db.root.albumsToMediaFiles.pairs --> let emptyAlbumIds = db.root.albumsToMediaFiles.pairs -->