WIP: Initial implementation of media library, simple threaded CLI.

This commit is contained in:
Jonathan Bernard 2022-10-18 23:22:10 -05:00
parent 8788015c4f
commit af12546ebc
13 changed files with 441 additions and 152 deletions

View File

@ -3,6 +3,10 @@ SOURCES := $(shell find src/main/nim -name "*.nim")
wdiwtlt.exe: $(SOURCES)
nimble build -d:mingw --cpu:amd64
.PHONY: install-win
install-win: wdiwtlt.exe
cp wdiwtlt.exe /home/jdb/winhome/programs/vlc-3.0.18
.PHONY: run-win
run-win: wdiwtlt.exe
cp wdiwtlt.exe /home/jdb/winhome/programs/vlc-3.0.18

View File

@ -33,7 +33,7 @@ WDIWTLT will be made up of multiple subprojects:
### `core`
`core` contains the data layer implementation, built with the fiber-orm
layer over PostgreSQL, and common functionality for managing a media library.
layer over SQLite, and common functionality for managing a media library.
### `cli`
@ -50,3 +50,11 @@ the WDIWTLT database (exposed by the core)
## Install
## Building From Source
### Linking to VLC
#### Windows
*WIP notes as I develop*
Copy `libvlc.dll` and `libvlccore.dll` from the VLC installation folder.

1
src/main/nim/config.nims Normal file
View File

@ -0,0 +1 @@
switch("threads", "on")

View File

@ -1,42 +1,21 @@
import std/json, std/logging
import docopt
import private/cliconstants
import private/libvlc
import wdiwtlt/config
import wdiwtlt/db
import wdiwtlt/library
type WdiwtltContext = ref object
cfg: WdiwtltConfig
player: VlcMediaPlayer
vlc: LibVlcInstance
library: WdiwtltLibrary
proc initContext(cfg: WdiwtltConfig): WdiwtltContext =
let vlc = newVlc()
return WdiwtltContext(
cfg: cfg,
player: vlc.newMediaPlayer,
vlc: vlc,
library: initLibrary(cfg.libraryPath, cfg.dbPath))
proc release(ctx: WdiwtltContext) =
if not ctx.player.isNil: ctx.player.release
if not ctx.vlc.isNil: ctx.vlc.release
import wdiwtlt/private/cliconstants
import wdiwtlt/[cli, config, db]
when isMainModule:
var ctx: WdiwtltContext
try:
let args = docopt(USAGE, version = WDIWTLT_VERSION)
let consoleLogger = newConsoleLogger(
#levelThreshold=lvlInfo,
levelThreshold=lvlDebug,
levelThreshold =
if args["--debug"]: lvlDebug
else: lvlInfo,
fmtStr="pit - $levelname: ")
logging.addHandler(consoleLogger)
let args = docopt(USAGE, version = WDIWTLT_VERSION)
let cfg = loadConfig(args)
if args["init-library"]:
@ -52,21 +31,9 @@ when isMainModule:
quit(0)
ctx = cfg.initContext
let media = ctx.vlc.mediaFromPath("file:///C:/Users/Jonathan Bernard/programs/vlc-3.0.18/2022-08-06.wav")
ctx.player.setMedia(media)
debug "Initialized LibVLC instance. Playing..."
discard ctx.player.play
discard stdin.readline
ctx.player.stop
media.release
startCli(cfg)
except:
fatal getCurrentExceptionMsg()
debug getStackTrace(getCurrentException())
quit(QuitFailure)
finally:
if not ctx.isNil: ctx.release
debug "Released LibVLC instance."

View File

@ -0,0 +1,81 @@
import std/[os, strutils]
import ./config, ./library, ./libvlc, ./models
import private/cliconstants
type CliCtx = ref object
cfg: WdiwtltConfig
library: WdiwtltLibrary
vlc: LibVlcInstance
player: VlcMediaPlayer
const STOP_CMDS = ["stop", "exit", "quit", ":q"]
var cmdChannel: Channel[string]
var readyForCmd: Channel[bool]
proc handleCmd(ctx: CliCtx, cmd: string): bool;
proc initCliCtx(cfg: WdiwtltConfig): CliCtx =
let vlc = newVlc()
return CliCtx(
cfg: cfg,
player: vlc.newMediaPlayer,
vlc: vlc,
library: initLibrary(cfg.libraryPath, cfg.dbPath))
proc release(ctx: CliCtx) =
if not ctx.player.isNil: ctx.player.release
if not ctx.vlc.isNil: ctx.vlc.release
proc readInputLoop() =
var line: string
while not STOP_CMDS.contains(line):
stdout.write("> ")
line = stdin.readline
cmdChannel.send(line)
while not readyForCmd.tryRecv().dataAvailable: sleep(200)
proc renderLoop(ctx: CliCtx) =
var shouldStop = false
while not shouldStop:
# poll for input
let cmdMsg = cmdChannel.tryRecv()
if cmdMsg.dataAvailable:
shouldStop = ctx.handleCmd(cmdMsg.msg)
readyForCmd.send(true)
# render
sleep(200)
proc startCli*(cfg: WdiwtltConfig) =
var ctx = cfg.initCliCtx
try:
stdout.writeLine("wdiwtlt v" & WDIWTLT_VERSION)
cmdChannel.open()
readyForCmd.open()
var readLineWorker: Thread[void]
createThread(readLineWorker, readInputLoop)
ctx.renderLoop()
readLineWorker.joinThread()
finally:
readyForCmd.close()
cmdChannel.close()
ctx.release()
proc processScan(ctx: CliCtx) =
ctx.library.scan
proc handleCmd(ctx: CliCtx, cmd: string): bool =
result = false
if STOP_CMDS.contains(cmd): return true
let cmdParts = cmd.split(" ")
if cmdParts.len == 0: return
case cmdParts[0]:
of "scan": ctx.processScan()
else: stdout.writeLine("Unrecognized command: '" & cmdParts[0] & "'")

View File

@ -27,6 +27,8 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): Wdiwt
var cfgFilename : string = cfgLocations -->
fold("", if len(it) > 0: it else: a)
debug "Loading config from '" & cfgFilename & "'"
if not fileExists(cfgFilename):
warn "could not find .wdiwtltrc file: " & cfgFilename
if isEmptyOrWhitespace(cfgFilename):
@ -39,7 +41,9 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): Wdiwt
finally: close(cfgFile)
var cfgJson: JsonNode
try: cfgJson = parseFile(cfgFilename)
try:
cfgJson = parseFile(cfgFilename)
debug "config values: \p" & cfgJson.pretty
except: raise newException(IOError,
"unable to read config file: " & cfgFilename &
"\p" & getCurrentExceptionMsg())
@ -47,6 +51,6 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): Wdiwt
let cfg = CombinedConfig(docopt: args, json: cfgJson)
result = WdiwtltConfig(
dbPath: cfg.getVal("db-path"),
libraryPath: cfg.getVal("library-location"),
libraryPath: cfg.getVal("library-path"),
cfgPath: cfgFilename,
cfg: cfg)

View File

@ -1,7 +1,7 @@
import std/[logging, json, jsonutils, options, os, tables, times]
import std/[logging, json, options, os, tables, times]
import timeutils, uuids, zero_functional
import ./models
import ./jsonutils, ./models
type
DbRoot = ref object
@ -41,6 +41,7 @@ proc debug*(db: WdiwtltDb): string =
## ========================================================================= ##
## API Contract ##
## ========================================================================= ##
proc initDbRoot(): DbRoot;
proc initDb*(path: string): WdiwtltDb;
proc loadDb*(path: string): WdiwtltDb;
proc persist*(db: WdiwtltDb): void;
@ -108,98 +109,124 @@ 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 ##
## ========================================================================= ##
## To JSON
## --------------------
proc `%`(dt: DateTime): JsonNode = %(dt.formatIso8601)
proc `%`(table: TableRef): JsonNode =
result = newJObject()
for k, v in table.pairs: result[$k] = %v
proc `%`(tup: tuple[albumId, mediaFileId: UUID]): JsonNode =
result = newJObject()
result["albumId"] = %($tup.albumId)
result["mediaFileId"] = %($tup.mediaFileId)
proc `%`(tup: tuple[artistId, mediaFileId: UUID]): JsonNode =
result = newJObject()
result["artistId"] = %($tup.artistId)
result["mediaFileId"] = %($tup.mediaFileId)
proc `%`(tup: tuple[artistId, albumId: UUID]): JsonNode =
result = newJObject()
result["artistId"] = %($tup.artistId)
result["albumId"] = %($tup.albumId)
proc `%`(tup: tuple[tagName: string, mediaFileId: UUID]): JsonNode =
result = newJObject()
result["tagName"] = %($tup.tagName)
result["mediaFileId"] = %($tup.mediaFileId)
proc `%`(root: DbRoot): JsonNode =
%*{
"albums": root.albums,
"artists": root.artists,
"bookmarks": root.bookmarks,
"mediaFiles": root.mediaFiles,
"playlists": root.playlists,
"tags": root.tags,
"albumsToMediaFiles": root.albumsToMediaFiles,
"artistsToMediaFiles": root.artistsToMediaFiles,
"artistsAndAlbums": root.artistsAndAlbums,
"playlistsToMediaFiles": root.playlistsToMediaFiles,
"tagsAndMediaFiles": root.tagsAndMediaFiles,
"mediaFileHashToId": root.mediaFileHashToId
}
## From JSON
## --------------------
proc parseAlbumsTable(n: JsonNode): TableRef[UUID, Album] =
result = newTable[UUID, Album]()
for strId in n.keys: result[parseUUID(strId)] = parseAlbum(n[strId])
proc parseArtistsTable(n: JsonNode): TableRef[UUID, Artist] =
result = newTable[UUID, Artist]()
for strId in n.keys: result[parseUUID(strId)] = parseArtist(n[strId])
proc parseBookmarksTable(n: JsonNode): TableRef[UUID, Bookmark] =
result = newTable[UUID, Bookmark]()
for strId in n.keys: result[parseUUID(strId)] = parseBookmark(n[strId])
proc parseMediaFilesTable(n: JsonNode): TableRef[UUID, MediaFile] =
result = newTable[UUID, MediaFile]()
for strId in n.keys: result[parseUUID(strId)] = parseMediaFile(n[strId])
proc parsePlaylistsTable(n: JsonNode): TableRef[UUID, Playlist] =
result = newTable[UUID, Playlist]()
for strId in n.keys: result[parseUUID(strId)] = parsePlaylist(n[strId])
proc parseTagsTable(n: JsonNode): TableRef[string, Option[string]] =
result = newTable[string, Option[string]]()
for tagName in n.keys:
if n[tagName].getStr("").len > 0: result[tagName] = some(n[tagName].getStr)
else: result[tagName] = none[string]()
proc parseMediaFilesAssociationTable(n: JsonNode): TableRef[UUID, seq[UUID]] =
result = newTable[UUID, seq[UUID]]()
for strId in n.keys:
result[parseUUID(strId)] = n[strId].getElems --> map(parseUUID(it.getStr))
proc parseArtistsAndAlbumsList(n: JsonNode):
seq[tuple[artistId, albumId: UUID]] =
n.getElems --> map((it.parseUUID("artistId"), it.parseUUID("albumId")))
proc parseTagsAndMediaFilesList(n: JsonNode):
seq[tuple[tagName: string, mediaFileId: UUID]] =
n.getElems --> map((it.getOrFail("tagName").getStr, it.parseUUID("mediaFileId")))
proc parseMediaFileHashToIdTable(n: JsonNode): TableRef[string, UUID] =
result = newTable[string, UUID]()
for hash in n.keys: result[hash] = parseUUID(n[hash].getStr)
proc parseDbRoot(n: JsonNode): DbRoot =
result = DbRoot(
albums: n.getOrFail("albums").parseAlbumsTable,
artists: n.getOrFail("artists").parseArtistsTable,
bookmarks: n.getOrFail("bookmarks").parseBookmarksTable,
mediaFiles: n.getOrFail("mediaFiles").parseMediaFilesTable,
playlists: n.getOrFail("playlists").parsePlaylistsTable,
tags: n.getOrFail("tags").parseTagsTable,
albumsToMediaFiles:
n.getOrFail("albumsToMediaFiles").parseMediaFilesAssociationTable,
artistsToMediaFiles:
n.getOrFail("artistsToMediaFiles").parseMediaFilesAssociationTable,
artistsAndAlbums:
n.getOrFail("artistsAndAlbums").parseArtistsAndAlbumsList,
playlistsToMediaFiles:
n.getOrFail("playlistsToMediaFiles").parseMediaFilesAssociationTable,
tagsAndMediaFiles:
n.getOrFail("tagsAndMediaFiles").parseTagsAndMediaFilesList,
mediaFileHashToId:
n.getOrFail("mediaFileHashToId").parseMediaFileHashToIdTable
)
## 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) =
u = parseUUID(node.getStr)
proc toJsonHook(dt: DateTime): JsonNode = %(dt.formatIso8601)
proc fromJsonHook(dt: var DateTime, node: JsonNode) =
dt = parseIso8601(node.getStr)
proc initDbRoot(): DbRoot =
DbRoot(
albums: newTable[UUID, Album](),
@ -224,19 +251,14 @@ proc loadDb*(path: string): WdiwtltDb =
if not fileExists(path):
raise newException(Exception, "Unable to open database file '" & path & "'")
let jsonRoot = parseJson(path.readFile)
var root: DbRoot = initDbRoot()
root.fromJson(jsonRoot)
debug "loaded DB"
result = WdiwtltDb(
jsonFilePath: path,
root: root)
root: parseDbRoot(parseJson(path.readFile)))
debug result.debug
proc persist*(db: WdiwtltDb): void =
let jsonRoot = db.root.toJson
db.jsonFilePath.writeFile($jsonRoot)
db.jsonFilePath.writeFile($(%db.root))
## Albums
## --------------------

View File

@ -0,0 +1,15 @@
## JSON parsing utils
import json, times, timeutils, uuids
proc getOrFail*(n: JsonNode, key: string): JsonNode =
## convenience method to get a key from a JObject or raise an exception
if not n.hasKey(key):
raise newException(ValueError, "missing key '" & key & "'")
return n[key]
proc parseUUID*(n: JsonNode, key: string): UUID =
return parseUUID(n.getOrFail(key).getStr)
proc parseIso8601*(n: JsonNode, key: string): DateTime =
return parseIso8601(n.getOrFail(key).getStr)

View File

@ -1,11 +1,28 @@
import times
import ./db, ./models
import std/[nre, options, os, sha1, strutils, times, unicode]
import console_progress, uuids
import ./db, ./libvlc, ./models
type
WdiwtltLibrary* = ref object
rootPath: string
#autoDeletePeriodMs*: int # 1000 * 60 * 60 * 24 * 6 # one week
db: WdiwtltDb
vlc: LibVlcInstance
let FILENAME_PAT = re"(\d+[:-_ ]+)?(.+)$"
let RECOGNIZED_MEDIA_EXTENSIONS = [
"3gp", "aac", "aif", "avi", "div", "flac", "flv", "h264", "m4a", "mid",
"midi", "mka", "mkv", "mov", "mp3", "mp4a", "mpeg", "mpg", "mpg3", "mpg4",
"mpga", "ogg", "vorb", "wav", "wma", "wma1", "wma2", "wmv", "wmv1", "wmv2",
"x264"
]
iterator walkMediaFiles*(l: WdiwtltLibrary): string =
for f in l.rootPath.walkDirRec(relative = true):
let (_, name, ext) = f.splitFile
if name.startsWith('.'): continue
if not RECOGNIZED_MEDIA_EXTENSIONS.contains(ext.toLower): continue
yield f
proc initLibrary*(rootPath: string, dbPath: string): WdiwtltLibrary =
WdiwtltLibrary(rootPath: rootPath, db: loadDb(dbPath))
@ -19,3 +36,148 @@ proc clean*(l: WdiwtltLibrary) =
l.db.pruneStalePlaylists(staleDt)
l.db.pruneStaleBookmarks(staleDt)
l.db.persist
proc initMediaFile*(
l: WdiwtltLibrary,
path: string,
id = none[UUID](),
hash = none[string]()
): tuple[
mf: MediaFile,
artistName: Option[string],
albumName: Option[string],
trackTotal: Option[int]
] =
if not fileExists(path):
raise newException(IOError, "file does not exist: '" & path & "'")
let m = path.match(FILENAME_PAT)
result = (
MediaFile(
comment: none[string](),
dateAdded: now(),
discNumber: none[string](),
fileHash:
if hash.isSome: hash.get
else: $secureHashFile(path),
filePath:
if m.isSome: m.get.captures[2]
else: path,
id:
if id.isSome: id.get
else: genUUID(),
imageUri: none[string](),
lastPlayed: none[DateTime](),
metaInfoSource: msFileLocation,
name: path.splitFile.name,
playCount: 0,
presentLocally: true,
trackNumber:
if m.isSome and m.get.captures.contains(1):
some(parseInt(m.get.captures[1]))
else: none[int]()),
none[string](),
none[string](),
none[int]())
var media: VlcMedia
try:
media = l.vlc.mediaFromPath(path)
media.parse
let mName = media.getMeta(vmTitle)
if not mName.isNil: result.mf.name = $mName
let mDiscNo = media.getMeta(vmDiscNumber)
if not mDiscNo.isNil: result.mf.discNumber = some($mDiscNo)
let mImgUri = media.getMeta(vmArtworkURL)
if not mImgUri.isNil: result.mf.imageUri = some($mImgUri)
let mTrackNo = media.getMeta(vmTrackNumber)
if not mTrackNo.isNil: result.mf.trackNumber = some(parseInt($mTrackNo))
result.mf.metaInfoSource = msTagInfo
let mAlbum = media.getMeta(vmAlbum)
if not mAlbum.isNil: result.albumName = some($mAlbum)
let mArtist = media.getMeta(vmArtist)
if not mArtist.isNil: result.artistName = some($mArtist)
let mTrackTotal = media.getMeta(vmTrackTotal)
if not mTrackTotal.isNil:
try: result.trackTotal = some(parseInt($mTrackTotal))
except: result.trackTotal = none[int]()
except: discard
finally:
if not media.isNil: media.release
proc scan*(l: WdiwtltLibrary) =
var fileCount = 0
for f in l.walkMediaFiles: fileCount += 1
let progress = newProgress(stdout, fileCount)
var curCount = 0
for f in l.walkMediaFiles:
progress.updateProgress(curCount, f[max(f.high - 15, 0)..f.high])
# Skip this file if we already have a record of it
let hash = $secureHashFile(f)
var existingMf = l.db.findMediaFileByHash(hash)
if existingMf.isSome: continue
# Process this new file
let (mf, artistsFromMeta, albumsFromMeta, trackTotal) = l.initMediaFile(f)
l.db.add(mf)
var allArtists = newSeq[Artist]()
var allAlbums = newSeq[Album]()
# Associate the file with its albums, creating album records if necessary
if albumsFromMeta.isSome:
let albumNames = albumsFromMeta.get.split(";")
for name in albumNames:
let existing = l.db.findAlbumsByName(name)
allAlbums &= existing
if existing.len > 0: l.db.add(existing[0], mf)
else:
let newAlbum = Album(
id: genUUID(),
name: name,
imageUri: mf.imageUri,
trackTotal:
if trackTotal.isSome: trackTotal.get
else: 0)
allAlbums.add(newAlbum)
l.db.add(newAlbum)
l.db.add(newAlbum, mf)
# Associate the file with its artists, creating artist records if necessary
if artistsFromMeta.isSome:
let artistNames = artistsFromMeta.get.split(";")
for name in artistNames:
let existing = l.db.findArtistsByName(name)
allArtists &= existing
if existing.len > 0: l.db.add(existing[0], mf)
else:
let newArtist = Artist(
id: genUUID(),
name: name,
imageUri: mf.imageUri)
allArtists.add(newArtist)
l.db.add(newArtist)
l.db.add(newArtist, mf)
# Make sure we have association records between the artists and the albums
if allAlbums.len > 0 and allArtists.len > 0:
for album in allAlbums:
for artist in allArtists:
l.db.associate(allArtists[0], allAlbums[0])
l.db.persist

View File

@ -1,5 +1,5 @@
import std/json, std/options, std/strutils, std/times
import uuids
import std/[json, jsonutils, options, strutils, times]
import timeutils, uuids
type
MetaSource* = enum msTagInfo = "tag info", msFileLocation = "file location"
@ -10,7 +10,7 @@ type
Album* = object of BaseModel
imageUri*: Option[string]
trackTotal: int
trackTotal*: int
year*: Option[int]
Artist* = object of BaseModel
@ -26,13 +26,13 @@ type
lastUsed*: DateTime
MediaFile* = object of BaseModel
comment*: string
comment*: Option[string]
dateAdded*: DateTime
discNumber*: Option[string]
fileHash*: string
filePath*: string
imageUri*: Option[string]
lastPlayed*: DateTime
lastPlayed*: Option[DateTime]
metaInfoSource*: MetaSource
playCount*: int
presentLocally*: bool
@ -62,3 +62,25 @@ proc `$`*(m: BaseModel): string = m.name
proc `$`*(t: Tag): string =
result = t.name
if t.description.isSome: result &= t.description.get
proc toJsonHook(uuid: UUID): JsonNode = %($uuid)
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)
proc `%`*(a: Album): JsonNode = toJson(a)
proc `%`*(a: Artist): JsonNode = toJson(a)
proc `%`*(b: Bookmark): JsonNode = toJson(b)
proc `%`*(mf: MediaFile): JsonNode = toJson(mf)
proc `%`*(p: Playlist): JsonNode = toJson(p)
proc `%`*(t: Tag): JsonNode = toJson(t)
proc parseAlbum*(n: JsonNode): Album = result.fromJson(n)
proc parseArtist*(n: JsonNode): Artist = result.fromJson(n)
proc parseBookmark*(n: JsonNode): Bookmark = result.fromJson(n)
proc parseMediaFile*(n: JsonNode): MediaFile = result.fromJson(n)
proc parsePlaylist*(n: JsonNode): Playlist = result.fromJson(n)
proc parseTag*(n: JsonNode): Tag = result.fromJson(n)

View File

@ -1,5 +1,3 @@
import json
const WDIWTLT_VERSION* = "1.0.0"
const USAGE* = "wdiwtlt v" & WDIWTLT_VERSION & """
@ -15,13 +13,17 @@ Options:
Config file for wdiwtlt CLI.
-L --library-root <root-dir>
-L --library-path <root-dir>
The path to a local media library directory.
-D --database-path <database-path>
-D --db-path <database-path>
The path to the JSON database file.
--debug
Enable debug logging.
"""
const ONLINE_HELP* = ""

View File

@ -17,6 +17,7 @@ requires @["docopt", "jester", "uuids", "zero_functional"]
requires "https://git.jdb-software.com/jdb/nim-cli-utils.git >= 0.6.3"
requires "https://git.jdb-software.com/jdb/nim-time-utils.git"
requires "https://git.jdb-software.com/jdb/update-nim-package-version"
requires "https://git.jdb-software.com/jdb/console-progress"
task updateVersion, "Update the WDIWTLT version.":
exec "update_nim_package_version wdiwtlt 'src/main/nim/private/version.nim'"