From 3f3a6b286bc89650e7f7d96c16dee46fc3d55e0b Mon Sep 17 00:00:00 2001 From: Jonathan Bernard Date: Sun, 23 Feb 2025 08:23:24 -0600 Subject: [PATCH] Add DB implementation, models, logging. --- .gitignore | 2 + config.nims | 4 ++ src/main/nim/wdiwtlt.nim | 2 + src/main/nim/wdiwtlt/db.nim | 58 +++++++++++++++++++++++++-- src/main/nim/wdiwtlt/logging.nim | 45 +++++++++++++++++++++ src/main/nim/wdiwtlt/medialibrary.nim | 33 ++++++++++++++- src/main/nim/wdiwtlt/models.nim | 4 +- wdiwtlt.nimble | 5 ++- 8 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 config.nims create mode 100644 src/main/nim/wdiwtlt/logging.nim diff --git a/.gitignore b/.gitignore index 612227b..a893313 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ *.sw? /wdiwtlt +nimble.develop +nimble.paths diff --git a/config.nims b/config.nims new file mode 100644 index 0000000..8ee48d2 --- /dev/null +++ b/config.nims @@ -0,0 +1,4 @@ +# begin Nimble config (version 2) +when withDir(thisDir(), system.fileExists("nimble.paths")): + include "nimble.paths" +# end Nimble config diff --git a/src/main/nim/wdiwtlt.nim b/src/main/nim/wdiwtlt.nim index cf58fee..ab4a356 100644 --- a/src/main/nim/wdiwtlt.nim +++ b/src/main/nim/wdiwtlt.nim @@ -1,6 +1,8 @@ import std/[os, uri] import mpv +import wdiwtlt/[models, db] + when isMainModule: var ctx: ptr handle try: diff --git a/src/main/nim/wdiwtlt/db.nim b/src/main/nim/wdiwtlt/db.nim index 6703d74..97a49bb 100644 --- a/src/main/nim/wdiwtlt/db.nim +++ b/src/main/nim/wdiwtlt/db.nim @@ -1,9 +1,7 @@ -import std/[json, jsonutils, options, sequtils, strutils, times] +import std/[json, options, sequtils, strutils, times] import db_connector/db_sqlite import waterpark/sqlite -import fiber_orm, namespaced_logging, timeutils - -import ./models +import fiber_orm, timeutils, uuids export fiber_orm.NotFoundError export fiber_orm.PaginationParams @@ -11,6 +9,8 @@ export fiber_orm.PagedRecords export fiber_orm.enableDbLogging export sqlite.close +import ./[logging, models] + type WdiwtltDb* = SqlitePool @@ -25,3 +25,53 @@ proc initDB*(dbPath: string): SqlitePool = generateProcsForModels(WdiwtltDb, [Artist, Album, MediaFile, Tag, Playlist, Bookmark, Image]) + +generateJoinTableLookups(WdiwtltDb, Artist, Album, "artists_albums") +generateJoinTableLookups(WdiwtltDb, Artist, MediaFile, "artists_media_files") +generateJoinTableLookups(WdiwtltDb, Album, MediaFile, "albums_media_files") +generateJoinTableLookups(WdiwtltDb, Playlist, MediaFile, "playlists_media_files") +generateJoinTableLookups(WdiwtltDb, MediaFile, Tag, "media_files_tags") +generateJoinTableLookups(WdiwtltDb, Artist, Image, "artists_images") +generateJoinTableLookups(WdiwtltDb, Album, Image, "albums_images") + +proc removeEmptyAlbums*(sql: WdiwtltDb) = + ## Remove albums that have no media files. + + let query = """ + DELETE FROM albums WHERE id IN ( + SELECT DISTINCT al.id + FROM albums al LEFT OUTER JOIN albums_media_files almf ON + al.id = almf.album_id + WHERE almf.album_id IS NULL)""" + + getLogger("wdiwtlt/db").debug(%*{ + "msg": "Deleting empty albums.", + "query": query}) + +proc removeEmptyArtists*(sql: WdiwtltDb) = + ## Remove artists that have no albums. + + let query = """ + DELETE FROM artists WHERE id IN ( + SELECT DISTINCT ar.id + FROM artists ar LEFT OUTER JOIN artists_albums aral ON + ar.id = aral.artist_id + WHERE aral.artist_id IS NULL)""" + + getLogger("wdiwtlt/db").debug(%*{ + "msg": "Deleting empty artists.", + "query": query}) + +proc removeEmptyPlaylists*(sql: WdiwtltDb) = + ## Remove playlists that have no media files. + + let query = """ + DELETE FROM playlists WHERE id IN ( + SELECT DISTINCT pl.id + FROM playlists pl LEFT OUTER JOIN playlists_media_files plmf ON + pl.id = plmf.playlist_id + WHERE plmf.playlist_id IS NULL)""" + + getLogger("wdiwtlt/db").debug(%*{ + "msg": "Deleting empty playlists.", + "query": query}) diff --git a/src/main/nim/wdiwtlt/logging.nim b/src/main/nim/wdiwtlt/logging.nim new file mode 100644 index 0000000..9f81a4b --- /dev/null +++ b/src/main/nim/wdiwtlt/logging.nim @@ -0,0 +1,45 @@ +import std/[options, os, strutils, unicode] +import namespaced_logging, zero_functional +import ./db + +export namespaced_logging + +var logService* {.threadvar.}: Option[LogService] + +proc enableLogging*(svc: LogService = initLogService(), debug = false): LogService = + if not (svc.cfg.appenders --> exists(it of ConsoleLogAppender)): + svc.addAppender(initConsoleLogAppender(threshold = lvlAll)) + + enableDbLogging(svc) + logService = some(svc) + result = svc + + +proc configureLoggingThresholds*(debug = false) = + if not logService.isSome: return + let logSvc = logService.get + + if debug: + logSvc.cfg.rootLevel = Level.lvlDebug + logSvc.cfg.loggers.add([ + LoggerConfig(name: "wdiwtlt", threshold: some(Level.lvlDebug)), + LoggerConfig(name: "fiber_orm", threshold: some(Level.lvlDebug)), + ]) + else: logSvc.cfg.rootLevel = Level.lvlInfo + + logSvc.reloadThreadState() + + +proc enableLoggingByEnvVar*(envVar = "DEBUG"): void = + if not logService.isSome: discard enableLogging() + let val = getEnv(envVar, "false").toLower() + + configureLoggingThresholds( + "true".startsWith(val) or + "yes".startsWith(val) or + "on".startsWith(val) or + val == "1") + +proc getLogger*(name: string): Option[Logger] = + if logService.isSome: return some(logService.get.getLogger(name)) + else: return none[Logger]() diff --git a/src/main/nim/wdiwtlt/medialibrary.nim b/src/main/nim/wdiwtlt/medialibrary.nim index 9cebb52..02d51ba 100644 --- a/src/main/nim/wdiwtlt/medialibrary.nim +++ b/src/main/nim/wdiwtlt/medialibrary.nim @@ -1,4 +1,4 @@ -import std/[paths] +import std/[paths, times] import namespaced_logging import ./[db, models] @@ -7,3 +7,34 @@ type MediaLibrary* = ref object db: WdiwtltDb libraryRoot: Path + +proc clean*(lib: MediaLibrary) = + removeEmptyAlbums(lib.db) + removeEmptyArtists(lib.db) + removeEmptyPlaylists(lib.db) + + let expirationDate = now() - weeks(1) + + let expiredPlaylists = lib.db.findPlaylstsWhere( + "user_created = false AND last_used_at < ?", + [expirationDate]) + + let expiredBookmarks = lib.db.findBookmarksWhere( + "user_created = false AND last_used_at < ?", + [expirationDate]) + + expiredPlaylists.applyIt(lib.db.deletePlaylist(it.id)) + expiredBookmarks.applyIt(lib.db.deleteBookmark(it.id)) + +proc rescanLibrary*(lib: MediaLibrary) = + # TODO: Implement this. Below is AI-generated code. + let mediaFiles = lib.db.getAllMediaFiles() + for mf in mediaFiles: + let filePath = lib.libraryRoot / mf.filePath + if not filePath.existsFile: + lib.db.deleteMediaFile(mf.id) + continue + + let fileHash = filePath.hashFile + if fileHash != mf.fileHash: + lib.db.updateMediaFile(mf.id, fileHash: fileHash) diff --git a/src/main/nim/wdiwtlt/models.nim b/src/main/nim/wdiwtlt/models.nim index 510f86b..30f21f8 100644 --- a/src/main/nim/wdiwtlt/models.nim +++ b/src/main/nim/wdiwtlt/models.nim @@ -1,4 +1,4 @@ -import std/[options, paths, times] +import std/[options, times] import uuids type @@ -18,7 +18,7 @@ type discNumber*: int trackNumber*: Option[int] playCount*: int - filePath*: Path + filePath*: string fileHash*: string metaInfoSource*: string dateAdded*: DateTime diff --git a/wdiwtlt.nimble b/wdiwtlt.nimble index 4d347ab..560d6ea 100644 --- a/wdiwtlt.nimble +++ b/wdiwtlt.nimble @@ -11,4 +11,7 @@ bin = @["wdiwtlt"] # Dependencies requires "nim >= 2.2.0" -requires @["mpv", "nimterop"] +requires @["mpv", "nimterop", "uuids", "waterpark"] + +# Dependencies from https://git.jdb-software.com/jdb/nim-packages +requires @["db_migrate", "fiber_orm"]