Implement selection, formatted text, help contents.
This commit is contained in:
parent
00a49723a9
commit
d15ea02a15
@ -1,23 +1,32 @@
|
||||
import std/json, std/logging
|
||||
import std/[json, logging, options]
|
||||
import docopt
|
||||
|
||||
import wdiwtlt/private/cliconstants
|
||||
import wdiwtlt/[cli, config, db]
|
||||
|
||||
when isMainModule:
|
||||
try:
|
||||
|
||||
var logFile: File
|
||||
try:
|
||||
let args = docopt(USAGE, version = WDIWTLT_VERSION)
|
||||
|
||||
let consoleLogger = newConsoleLogger(
|
||||
levelThreshold =
|
||||
if args["--debug"]: lvlDebug
|
||||
if args["--verbose"]: lvlDebug
|
||||
else: lvlInfo,
|
||||
fmtStr="pit - $levelname: ")
|
||||
logging.addHandler(consoleLogger)
|
||||
|
||||
let cfg = loadConfig(args)
|
||||
|
||||
if cfg.logFile.isSome:
|
||||
info "Logging to '" & cfg.logFile.get & "'"
|
||||
try: logFile = open(cfg.logFile.get)
|
||||
except:
|
||||
warn "Unable to open the log file '" & cfg.logFile.get & "'"
|
||||
let fileLogger = newFileLogger(logFile, lvlDebug)
|
||||
logging.addHandler(fileLogger)
|
||||
|
||||
if args["init-library"]:
|
||||
info "Initializing a new library database at '" & cfg.dbPath & "'"
|
||||
let newDb = initDb(cfg.dbPath)
|
||||
@ -37,3 +46,6 @@ when isMainModule:
|
||||
fatal getCurrentExceptionMsg()
|
||||
debug getStackTrace(getCurrentException())
|
||||
quit(QuitFailure)
|
||||
|
||||
finally:
|
||||
if not logFile.isNil: logFile.close()
|
||||
|
@ -1,21 +1,38 @@
|
||||
import std/[os, strutils]
|
||||
import ./config, ./library, ./libvlc, ./models
|
||||
import private/cliconstants
|
||||
import std/[algorithm, logging, nre, options, os, random, strutils]
|
||||
import uuids, zero_functional
|
||||
import ./config, ./db, ./library, ./libvlc, ./models, ./whenmatched
|
||||
import private/cliconstants, private/styledtext
|
||||
|
||||
type CliCtx = ref object
|
||||
cfg: WdiwtltConfig
|
||||
library: WdiwtltLibrary
|
||||
vlc: LibVlcInstance
|
||||
player: VlcMediaPlayer
|
||||
type
|
||||
|
||||
Selection = ref object
|
||||
models*: seq[BaseModel]
|
||||
|
||||
CliCtx = ref object
|
||||
cfg: WdiwtltConfig
|
||||
library: WdiwtltLibrary
|
||||
player: VlcMediaPlayer
|
||||
vlc: LibVlcInstance
|
||||
|
||||
currentBookmark: Option[Bookmark]
|
||||
currentSelection: Option[Selection]
|
||||
playQueue: Option[Playlist]
|
||||
curMediaFile: Option[MediaFile]
|
||||
|
||||
const STOP_CMDS = ["stop", "exit", "quit", ":q"]
|
||||
var cmdChannel: Channel[string]
|
||||
var readyForCmd: Channel[bool]
|
||||
|
||||
proc initSelection(models: seq[SomeModel]): Selection =
|
||||
Selection(models: models --> map(it.BaseModel))
|
||||
|
||||
proc handleCmd(ctx: CliCtx, cmd: string): bool;
|
||||
|
||||
proc err(msg: string) = raise newException(CliErr, msg)
|
||||
|
||||
proc initCliCtx(cfg: WdiwtltConfig): CliCtx =
|
||||
let vlc = newVlc()
|
||||
randomize()
|
||||
return CliCtx(
|
||||
cfg: cfg,
|
||||
player: vlc.newMediaPlayer,
|
||||
@ -35,7 +52,6 @@ proc readInputLoop() =
|
||||
|
||||
while not readyForCmd.tryRecv().dataAvailable: sleep(200)
|
||||
|
||||
|
||||
proc renderLoop(ctx: CliCtx) =
|
||||
var shouldStop = false
|
||||
while not shouldStop:
|
||||
@ -70,18 +86,133 @@ proc processScan(ctx: CliCtx, fullRescan = false) =
|
||||
stdout.writeLine("Sanning media library...")
|
||||
ctx.library.scan(fullRescan)
|
||||
|
||||
const SELECTABLE_MODELS = "album|artist|file|playlist|tag"
|
||||
|
||||
proc selectFrom(ctx: CliCtx, mf: MediaFile, modelType: ModelType): Selection =
|
||||
let db = ctx.library.db
|
||||
case modelType:
|
||||
of mtAlbum: result = initSelection(db.findAlbumsByMediaFile(mf))
|
||||
of mtArtist: result = initSelection(db.findArtistsByMediaFile(mf))
|
||||
of mtBookmark:
|
||||
if ctx.currentBookmark.isSome:
|
||||
result = initSelection(@[ctx.currentBookmark.get])
|
||||
else: err "no current bookmark to select"
|
||||
|
||||
of mtMediaFile: result = initSelection(@[mf])
|
||||
of mtPlaylist:
|
||||
if ctx.playQueue.isSome: result = initSelection(@[ctx.playQueue.get])
|
||||
else: err "no current playlist to select"
|
||||
of mtTag: result = initSelection(db.findTagsByMediaFile(mf))
|
||||
|
||||
proc selectRandom(
|
||||
ctx: CliCtx,
|
||||
count: int,
|
||||
source: Selection,
|
||||
mt: ModelType
|
||||
): Selection =
|
||||
|
||||
var options = ctx.library.db.findAllRelatedModels(source.models, mt)
|
||||
if count >= options.len: return initSelection(options)
|
||||
|
||||
shuffle(options)
|
||||
return initSelection(options[0..<count])
|
||||
|
||||
proc select(ctx: CliCtx, criteria: string): Selection =
|
||||
debug "Selecting. Options: " & criteria
|
||||
let db = ctx.library.db
|
||||
|
||||
# playing
|
||||
criteria.whenMatched(re("playing ($#)" % SELECTABLE_MODELS)):
|
||||
if ctx.curMediaFile.isNone: err "Nothing selected."
|
||||
|
||||
return ctx.selectFrom(
|
||||
ctx.curMediaFile.get,
|
||||
mapModelType(match.captures[0]))
|
||||
|
||||
# <count> random <model-type>s from <selection-criteria>
|
||||
criteria.whenMatched(re(
|
||||
"(\\d+ )?random ($#)s?( from (.+$))?" % SELECTABLE_MODELS)):
|
||||
|
||||
let count =
|
||||
if match.captures.contains(0): parseInt(match.captures[0].strip)
|
||||
else: 1
|
||||
|
||||
let modelType = mapModelType(match.captures[1])
|
||||
let source: Selection =
|
||||
if match.captures.contains(3): ctx.select(match.captures[3].strip)
|
||||
else: initSelection(getAll(db, modelType))
|
||||
|
||||
return ctx.selectRandom(count, source, modelType)
|
||||
|
||||
# selected <model-ype>
|
||||
criteria.whenMatched(re("selected ($#)s?" % SELECTABLE_MODELS)):
|
||||
if ctx.currentSelection.isNone: err "Nothing is selected."
|
||||
let modelType = mapModelType(match.captures[0])
|
||||
|
||||
return initSelection(
|
||||
db.findAllRelatedModels(ctx.currentSelection.get.models, modelType))
|
||||
|
||||
# absent files
|
||||
criteria.whenMatched(re("absent files")):
|
||||
return initSelection(db.allMediaFiles --> filter(not it.presentLocally))
|
||||
|
||||
# files tagged as <tags>...
|
||||
criteria.whenMatched(re("files tagged( as){0,1}((\\s[^\\s]+)+)")):
|
||||
|
||||
let tags: seq[Tag] = match.captures[1].split(re"\s") -->
|
||||
map(it.strip).
|
||||
map(db.findTagByName(it)).
|
||||
filter(it.isSome).
|
||||
map(it.get)
|
||||
|
||||
if tags.len == 0: err "Nothing is selected."
|
||||
return initSelection(tags --> map(db.findMediaFilesByTag(it)).flatten())
|
||||
|
||||
# <model-type> from <selection-criteria>
|
||||
criteria.whenMatched(re("($#)s? from (.+)" % SELECTABLE_MODELS)):
|
||||
let modelType = mapModelType(match.captures[0])
|
||||
let source = ctx.select(match.captures[1])
|
||||
|
||||
if source.models.len > 0 and modelMatchesType(source.models[0], modelType):
|
||||
return source
|
||||
|
||||
return initSelection(db.findAllRelatedModels(source.models, modelType))
|
||||
|
||||
# <model-type> <id>
|
||||
#criteria.whenMatched(re("($#)s?((\\s\\[a-fA-F0-9]+)+)" % SELECTABLE_MODELS)):
|
||||
# let modelType = mapModelType(match.captures[0])
|
||||
# let ids = match.captures[1].split(re"\s") --> map(parseUUID(it))
|
||||
|
||||
proc processList(ctx: CliCtx, options: string) =
|
||||
debug "Listing things. Options: " & options
|
||||
|
||||
proc processHelp(ctx: CliCtx, options: string) =
|
||||
stdout.writeStyledText(
|
||||
case options:
|
||||
of "": HELP_GENERAL
|
||||
of "scan": HELP_SCAN
|
||||
of "rescan": HELP_RESCAN
|
||||
of "quit": HELP_QUIT
|
||||
else: FE & options & " is not a recognized command.\p"
|
||||
)
|
||||
|
||||
proc handleCmd(ctx: CliCtx, cmd: string): bool =
|
||||
result = false
|
||||
if STOP_CMDS.contains(cmd): return true
|
||||
try:
|
||||
result = false
|
||||
if STOP_CMDS.contains(cmd): return true
|
||||
|
||||
let cmdParts = cmd.split(" ", 1)
|
||||
if cmdParts.len == 0: return
|
||||
let command = cmdParts[0].toLower
|
||||
let rest =
|
||||
if cmdParts.len > 1: cmdParts[1]
|
||||
else: ""
|
||||
let cmdParts = cmd.split(" ", 1)
|
||||
if cmdParts.len == 0: return
|
||||
let command = cmdParts[0].toLower
|
||||
let rest =
|
||||
if cmdParts.len > 1: cmdParts[1]
|
||||
else: ""
|
||||
|
||||
case command:
|
||||
of "scan": ctx.processScan()
|
||||
of "rescan": ctx.processScan(true)
|
||||
else: stdout.writeLine("Unrecognized command: '" & cmdParts[0] & "'")
|
||||
case command:
|
||||
of "scan": ctx.processScan()
|
||||
of "rescan": ctx.processScan(true)
|
||||
of "list": ctx.processList(rest)
|
||||
of "help": ctx.processHelp(rest)
|
||||
else: stdout.writeLine("Unrecognized command: '" & cmdParts[0] & "'")
|
||||
except:
|
||||
stdout.writeStyledText(FE & getCurrentExceptionMsg() & FN)
|
||||
|
@ -1,10 +1,11 @@
|
||||
import std/json, std/logging, std/os, std/strutils, std/tables
|
||||
import std/[json, logging, options, os, strutils, tables]
|
||||
import cliutils, docopt, zero_functional
|
||||
|
||||
type WdiwtltConfig* = object
|
||||
dbPath*: string
|
||||
libraryPath*: string
|
||||
cfgPath*: string
|
||||
logFile*: Option[string]
|
||||
cfg*: CombinedConfig
|
||||
|
||||
const DEFAULT_CFG_CONTENTS = """{
|
||||
@ -54,3 +55,6 @@ proc loadConfig*(args: Table[string, Value] = initTable[string, Value]()): Wdiwt
|
||||
libraryPath: cfg.getVal("library-path"),
|
||||
cfgPath: cfgFilename,
|
||||
cfg: cfg)
|
||||
|
||||
try: result.logFile = some(cfg.getVal("log-file"))
|
||||
except: result.logFile = none[string]()
|
||||
|
@ -1,8 +1,10 @@
|
||||
import std/[logging, json, options, os, tables, times]
|
||||
import std/[logging, json, options, os, sets, tables, times]
|
||||
import timeutils, uuids, zero_functional
|
||||
|
||||
import ./jsonutils, ./models
|
||||
|
||||
from std/sequtils import deduplicate, toSeq
|
||||
|
||||
type
|
||||
DbRoot = ref object
|
||||
albums: TableRef[UUID, Album]
|
||||
@ -10,13 +12,14 @@ type
|
||||
bookmarks: TableRef[UUID, Bookmark]
|
||||
mediaFiles: TableRef[UUID, MediaFile]
|
||||
playlists: TableRef[UUID, Playlist]
|
||||
tags: TableRef[string, Option[string]]
|
||||
tags: TableRef[UUID, Tag]
|
||||
tagSet: HashSet[string]
|
||||
|
||||
albumsToMediaFiles: TableRef[UUID, seq[UUID]]
|
||||
artistsToMediaFiles: TableRef[UUID, seq[UUID]]
|
||||
artistsAndAlbums: seq[tuple[artistId, albumId: UUID]]
|
||||
playlistsToMediaFiles: TableRef[UUID, seq[UUID]]
|
||||
tagsAndMediaFiles: seq[tuple[tagName: string, mediaFileId: UUID]]
|
||||
tagsAndMediaFiles: seq[tuple[tagId: UUID, mediaFileId: UUID]]
|
||||
|
||||
mediaFileHashToId: TableRef[string, UUID]
|
||||
|
||||
@ -52,27 +55,45 @@ 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 allAlbums*(db: WdiwtltDb): seq[Album];
|
||||
|
||||
proc findAlbumById*(db: WdiwtltDb, id: UUID): Option[Album];
|
||||
proc findAlbumsByArtist*(db: WdiwtltDb, a: Artist): seq[Album];
|
||||
proc findAlbumsByMediaFile*(db: WdiwtltDb, mf: MediaFile): seq[Album];
|
||||
proc findAlbumsByName*(db: WdiwtltDb, name: string): seq[Album];
|
||||
|
||||
proc findAllRelatedAlbums*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[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);
|
||||
proc isAssociated*(db: WdiwtltDb, artist: Artist, album: Album): bool;
|
||||
|
||||
proc allArtists*(db: WdiwtltDb): seq[Artist];
|
||||
|
||||
proc findArtistById*(db: WdiwtltDb, id: UUID): Option[Artist];
|
||||
proc findArtistsByAlbum*(db: WdiwtltDb, a: Album): seq[Artist];
|
||||
proc findArtistsByMediaFile*(db: WdiwtltDb, mf: MediaFile): seq[Artist];
|
||||
proc findArtistsByName*(db: WdiwtltDb, name: string): seq[Artist];
|
||||
|
||||
proc findAllRelatedArtists*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Artist];
|
||||
|
||||
## Bookmarks
|
||||
## --------------------
|
||||
@ -80,15 +101,39 @@ proc add*(db: WdiwtltDb, b: Bookmark);
|
||||
proc update*(db: WdiwtltDb, b: Bookmark);
|
||||
proc remove*(db: WdiwtltDb, b: Bookmark);
|
||||
|
||||
proc allBookmarks*(db: WdiwtltDb): seq[Bookmark];
|
||||
|
||||
proc findBookmarkById*(db: WdiwtltDb, id: UUID): Option[Bookmark];
|
||||
proc findBookmarksByPlaylist*(db: WdiwtltDb, p: Playlist): seq[Bookmark];
|
||||
proc findBookmarksByName*(db: WdiwtltDb, name: string): seq[Bookmark];
|
||||
|
||||
proc findAllRelatedBookmarks*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Bookmark];
|
||||
|
||||
## Media Files
|
||||
## --------------------
|
||||
proc add*(db: WdiwtltDb, mf: MediaFile);
|
||||
proc remove*(db: WdiwtltDb, mf: MediaFile);
|
||||
proc update*(db: WdiwtltDb, mf: MediaFile);
|
||||
|
||||
proc allMediaFiles*(db: WdiwtltDb): seq[MediaFile];
|
||||
|
||||
proc findMediaFileByHash*(db: WdiwtltDb, hash: string): Option[MediaFile];
|
||||
proc findMediaFileById*(db: WdiwtltDb, id: UUID): Option[MediaFile];
|
||||
proc findMediaFileByPath*(db: WdiwtltDb, path: string): Option[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];
|
||||
proc findMediaFileByPath*(db: WdiwtltDb, path: string): Option[MediaFile];
|
||||
proc findMediaFilesByBookmark*(db: WdiwtltDb, b: Bookmark): seq[MediaFile];
|
||||
proc findMediaFilesByName*(db: WdiwtltDb, name: string): seq[MediaFile];
|
||||
proc findMediaFilesByPlaylist*(db: WdiwtltDb, p: Playlist): seq[MediaFile];
|
||||
proc findMediaFilesByTag*(db: WdiwtltDb, tag: Tag): seq[MediaFile];
|
||||
|
||||
proc findAllRelatedMediaFiles*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[MediaFile];
|
||||
|
||||
## Playlists
|
||||
## --------------------
|
||||
@ -96,12 +141,45 @@ proc add*(db: WdiwtltDb, p: Playlist);
|
||||
proc remove*(db: WdiwtltDb, p: Playlist);
|
||||
proc update*(db: WdiwtltDb, p: Playlist);
|
||||
|
||||
proc allPlaylists*(db: WdiwtltDb): seq[Playlist];
|
||||
|
||||
proc findPlaylistById*(db: WdiwtltDb, id: UUID): Option[Playlist];
|
||||
proc findPlaylistsByMediaFile*(db: WdiwtltDb, mf: MediaFile): seq[Playlist];
|
||||
proc findPlaylistsByName*(db: WdiwtltDb, name: string): seq[Playlist];
|
||||
|
||||
proc findAllRelatedPlaylists*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Playlist];
|
||||
|
||||
## Tags
|
||||
## --------------------
|
||||
proc add*(db: WdiwtltDb, t: Tag);
|
||||
proc remove*(db: WdiwtltDb, t: Tag);
|
||||
proc update*(db: WdiwtltDb, t: Tag);
|
||||
|
||||
proc allTags*(db: WdiwtltDb): seq[Tag];
|
||||
|
||||
proc findTagById*(db: WdiwtltDb, id: UUID): Option[Tag];
|
||||
proc findTagByName*(db: WdiwtltDb, name: string): Option[Tag];
|
||||
proc findTagsByMediaFile*(db: WdiwtltDb, mf: MediaFile): seq[Tag];
|
||||
|
||||
proc findAllRelatedTags*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Tag];
|
||||
|
||||
## General
|
||||
## --------------------
|
||||
|
||||
proc getAll*(db: WdiwtltDb, mt: ModelType): seq[BaseModel];
|
||||
|
||||
proc findAllRelatedModels*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel],
|
||||
targetModelType: ModelType
|
||||
): seq[BaseModel];
|
||||
|
||||
## Housekeeping
|
||||
## --------------------
|
||||
proc pruneStalePlaylists*(db: WdiwtltDb, ts: DateTime);
|
||||
@ -137,9 +215,9 @@ proc `%`(tup: tuple[artistId, albumId: UUID]): JsonNode =
|
||||
result["artistId"] = %($tup.artistId)
|
||||
result["albumId"] = %($tup.albumId)
|
||||
|
||||
proc `%`(tup: tuple[tagName: string, mediaFileId: UUID]): JsonNode =
|
||||
proc `%`(tup: tuple[tagId: UUID, mediaFileId: UUID]): JsonNode =
|
||||
result = newJObject()
|
||||
result["tagName"] = %($tup.tagName)
|
||||
result["tagId"] = %($tup.tagId)
|
||||
result["mediaFileId"] = %($tup.mediaFileId)
|
||||
|
||||
proc `%`(root: DbRoot): JsonNode =
|
||||
@ -180,11 +258,9 @@ 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 parseTagsTable(n: JsonNode): TableRef[UUID, Tag] =
|
||||
result = newTable[UUID, Tag]()
|
||||
for strId in n.keys: result[parseUUID(strId)] = parseTag(n[strId])
|
||||
|
||||
proc parseMediaFilesAssociationTable(n: JsonNode): TableRef[UUID, seq[UUID]] =
|
||||
result = newTable[UUID, seq[UUID]]()
|
||||
@ -196,21 +272,22 @@ proc parseArtistsAndAlbumsList(n: JsonNode):
|
||||
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")))
|
||||
seq[tuple[tagId: UUID, mediaFileId: UUID]] =
|
||||
n.getElems --> map((it.parseUUID("tagId"), 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 =
|
||||
let tagsTable = n.getOrFail("tags").parseTagsTable
|
||||
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,
|
||||
tags: tagsTable,
|
||||
|
||||
albumsToMediaFiles:
|
||||
n.getOrFail("albumsToMediaFiles").parseMediaFilesAssociationTable,
|
||||
@ -226,6 +303,8 @@ proc parseDbRoot(n: JsonNode): DbRoot =
|
||||
n.getOrFail("mediaFileHashToId").parseMediaFileHashToIdTable
|
||||
)
|
||||
|
||||
result.tagSet = toHashSet(tagsTable.values.toSeq --> map(it.name))
|
||||
|
||||
|
||||
## Internals
|
||||
## --------------------
|
||||
@ -236,12 +315,13 @@ proc initDbRoot(): DbRoot =
|
||||
bookmarks: newTable[UUID, Bookmark](),
|
||||
mediaFiles: newTable[UUID, MediaFile](),
|
||||
playlists: newTable[UUID, Playlist](),
|
||||
tags: newTable[string, Option[string]](),
|
||||
tags: newTable[UUID, Tag](),
|
||||
albumsToMediaFiles: newTable[UUID, seq[UUID]](),
|
||||
artistsToMediaFiles: newTable[UUID, seq[UUID]](),
|
||||
artistsAndAlbums: @[],
|
||||
playlistsToMediaFiles: newTable[UUID, seq[UUID]](),
|
||||
tagsAndMediaFiles: @[],
|
||||
tagSet: initHashSet[string](),
|
||||
mediaFileHashToId: newTable[string, UUID]())
|
||||
|
||||
proc initDb*(path: string): WdiwtltDb =
|
||||
@ -273,16 +353,6 @@ proc remove*(db: WdiwtltDb, a: Album) =
|
||||
|
||||
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]
|
||||
@ -296,6 +366,52 @@ proc remove*(db: WdiwtltDb, a: Album, mf: MediaFile) =
|
||||
db.root.albumsToMediaFiles[a.id] =
|
||||
db.root.albumsToMediaFiles[a.id] --> filter(it != mf.id)
|
||||
|
||||
proc allAlbums*(db: WdiwtltDb): seq[Album] = db.root.albums.values.toSeq
|
||||
|
||||
proc findAlbumById*(db: WdiwtltDb, id: UUID): Option[Album] =
|
||||
if not db.root.albums.contains(id): return none[Album]()
|
||||
return some(db.root.albums[id])
|
||||
|
||||
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 findAlbumsByMediaFile*(db: WdiwtltDb, mf: MediaFile): seq[Album] =
|
||||
db.root.albumsToMediaFiles.pairs.toSeq -->
|
||||
filter(db.root.albums.contains(it[0]) and it[1].contains(mf.id)).
|
||||
map(db.root.albums[it[0]])
|
||||
|
||||
proc findAlbumsByName*(db: WdiwtltDb, name: string): seq[Album] =
|
||||
db.root.albums.values.toSeq --> filter(it.name == name)
|
||||
|
||||
proc findAllRelatedAlbums(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Album] =
|
||||
|
||||
if sourceModels.len == 0: return @[]
|
||||
let firstModel = sourceModels[0]
|
||||
|
||||
if firstModel of Album: return sourceModels --> map(it.Album)
|
||||
|
||||
elif firstModel of Artist:
|
||||
return sourceModels -->
|
||||
map(db.findAlbumsByArtist(it.Artist)).flatten()
|
||||
|
||||
elif firstModel of Bookmark or
|
||||
firstModel of Playlist or
|
||||
firstModel of MediaFile or
|
||||
firstModel of Tag:
|
||||
|
||||
return deduplicate(
|
||||
db.findAllRelatedMediaFiles(sourceModels) -->
|
||||
map(db.findAlbumsByMediaFile(it)).flatten())
|
||||
|
||||
else:
|
||||
warn "unknown source model type, returning empty selection"
|
||||
debug "findAllRelatedAlbums: first source model was " & $firstModel
|
||||
|
||||
## Artists
|
||||
## --------------------
|
||||
proc add*(db: WdiwtltDb, a: Artist) = db.root.artists[a.id] = a
|
||||
@ -308,15 +424,52 @@ proc remove*(db: WdiwtltDb, a: Artist) =
|
||||
|
||||
proc update*(db: WdiwtltDb, a: Artist) = db.add(a)
|
||||
|
||||
proc allArtists*(db: WdiwtltDb): seq[Artist] = db.root.artists.values.toSeq
|
||||
|
||||
proc findArtistById*(db: WdiwtltDb, id: UUID): Option[Artist] =
|
||||
if not db.root.artists.contains(id): return none[Artist]()
|
||||
return some(db.root.artists[id])
|
||||
|
||||
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 findArtistsByMediaFile*(db: WdiwtltDb, mf: MediaFile): seq[Artist] =
|
||||
db.root.artistsToMediaFiles.pairs.toSeq -->
|
||||
filter(db.root.artists.contains(it[0]) and it[1].contains(mf.id)).
|
||||
map(db.root.artists[it[0]])
|
||||
|
||||
proc findArtistsByName*(db: WdiwtltDb, name: string): seq[Artist] =
|
||||
result = @[]
|
||||
for a in db.root.artists.values:
|
||||
if a.name == name: result.add(a)
|
||||
db.root.artists.values.toSeq --> filter(it.name == name)
|
||||
|
||||
proc findAllRelatedArtists(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Artist] =
|
||||
|
||||
if sourceModels.len == 0: return @[]
|
||||
let firstModel = sourceModels[0]
|
||||
|
||||
if firstModel of Album:
|
||||
return deduplicate(
|
||||
sourceModels -->
|
||||
map(db.findArtistsByAlbum(it.Album)).flatten())
|
||||
|
||||
elif firstModel of Artist: return sourceModels --> map(it.Artist)
|
||||
|
||||
elif firstModel of Bookmark or
|
||||
firstModel of Playlist or
|
||||
firstModel of MediaFile or
|
||||
firstModel of Tag:
|
||||
|
||||
return deduplicate(
|
||||
db.findAllRelatedMediaFiles(sourceModels) -->
|
||||
map(db.findArtistsByMediaFile(it)).flatten())
|
||||
|
||||
else:
|
||||
warn "unknown source model type, returning empty selection"
|
||||
debug "findAllRelatedArtists: first source model was " & $firstModel
|
||||
|
||||
proc add*(db: WdiwtltDb, a: Artist, mf: MediaFile) =
|
||||
if not db.root.artistsToMediaFiles.contains(a.id):
|
||||
@ -354,6 +507,66 @@ 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)
|
||||
|
||||
proc allBookmarks*(db: WdiwtltDb): seq[Bookmark] =
|
||||
db.root.bookmarks.values.toSeq
|
||||
|
||||
proc findBookmarkById*(db: WdiwtltDb, id: UUID): Option[Bookmark] =
|
||||
if not db.root.bookmarks.contains(id): return none[Bookmark]()
|
||||
return some(db.root.bookmarks[id])
|
||||
|
||||
proc findBookmarksByName*(db: WdiwtltDb, name: string): seq[Bookmark] =
|
||||
db.root.bookmarks.values.toSeq --> filter(it.name == name)
|
||||
|
||||
proc findBookmarksByPlaylist*(db: WdiwtltDb, p: Playlist): seq[Bookmark] =
|
||||
db.root.bookmarks.values.toSeq -->
|
||||
filter(it.playlistId == p.id)
|
||||
|
||||
proc findAllRelatedBookmarks*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Bookmark] =
|
||||
|
||||
if sourceModels.len == 0: return @[]
|
||||
let firstModel = sourceModels[0]
|
||||
|
||||
if firstModel of Album:
|
||||
return deduplicate(
|
||||
sourceModels -->
|
||||
map(db.findMediaFilesByAlbum(it.Album)).flatten().
|
||||
map(db.findPlaylistsByMediaFile(it)).flatten().
|
||||
map(db.findBookmarksByPlaylist(it)).flatten())
|
||||
|
||||
elif firstModel of Artist:
|
||||
return deduplicate(
|
||||
sourceModels -->
|
||||
map(db.findMediaFilesByArtist(it.Artist)).flatten().
|
||||
map(db.findPlaylistsByMediaFile(it)).flatten().
|
||||
map(db.findBookmarksByPlaylist(it)).flatten())
|
||||
|
||||
elif firstModel of Bookmark:
|
||||
return sourceModels --> map(it.Bookmark)
|
||||
|
||||
elif firstModel of MediaFile:
|
||||
return deduplicate(
|
||||
sourceModels -->
|
||||
map(db.findPlaylistsByMediaFile(it.MediaFile)).flatten().
|
||||
map(db.findBookmarksByPlaylist(it)).flatten())
|
||||
|
||||
elif firstModel of Playlist:
|
||||
return deduplicate(
|
||||
sourceModels --> map(db.findBookmarksByPlaylist(it.Playlist)).flatten())
|
||||
|
||||
elif firstModel of Tag:
|
||||
return deduplicate(
|
||||
sourceModels -->
|
||||
map(db.findMediaFilesByTag(it.Tag)).flatten().
|
||||
map(db.findPlaylistsByMediaFile(it)).flatten().
|
||||
map(db.findBookmarksByPlaylist(it)).flatten())
|
||||
|
||||
else:
|
||||
warn "unknown source model type, returning empty selection"
|
||||
debug "findAllRelatedBookmarks: first source model was " & $firstModel
|
||||
|
||||
## Media Files
|
||||
## --------------------
|
||||
proc add*(db: WdiwtltDb, mf: MediaFile) =
|
||||
@ -371,19 +584,8 @@ proc remove*(db: WdiwtltDb, mf: MediaFile) =
|
||||
|
||||
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 allMediaFiles*(db: WdiwtltDb): seq[MediaFile] =
|
||||
db.root.mediaFiles.values.toSeq
|
||||
|
||||
proc findMediaFileByHash*(db: WdiwtltDb, hash: string): Option[MediaFile] =
|
||||
if db.root.mediaFileHashToId.contains(hash):
|
||||
@ -393,11 +595,87 @@ proc findMediaFileByHash*(db: WdiwtltDb, hash: string): Option[MediaFile] =
|
||||
|
||||
return none[MediaFile]()
|
||||
|
||||
proc findMediaFileById*(db: WdiwtltDb, id: UUID): Option[MediaFile] =
|
||||
if not db.root.mediaFiles.contains(id): return none[MediaFile]()
|
||||
return some(db.root.mediaFiles[id])
|
||||
|
||||
proc findMediaFileByPath*(db: WdiwtltDb, path: string): Option[MediaFile] =
|
||||
for mf in db.root.mediaFiles.values:
|
||||
if mf.filePath == path: return some(mf)
|
||||
return none[MediaFile]()
|
||||
|
||||
proc findMediaFilesByAlbum*(db: WdiwtltDb, a: Album): seq[MediaFile] =
|
||||
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] =
|
||||
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 findMediaFilesByBookmark*(db: WdiwtltDb, b: Bookmark): seq[MediaFile] =
|
||||
if db.root.playlists.contains(b.playlistId):
|
||||
return db.findMediaFilesByPlaylist(db.root.playlists[b.playlistId])
|
||||
|
||||
proc findMediaFilesByName*(db: WdiwtltDb, name: string): seq[MediaFile] =
|
||||
db.root.mediaFiles.values.toSeq --> filter(it.name == name)
|
||||
|
||||
proc findMediaFilesByPlaylist*(db: WdiwtltDb, p: Playlist): seq[MediaFile] =
|
||||
if db.root.playlistsToMediaFiles.contains(p.id):
|
||||
return db.root.playlistsToMediaFiles[p.id] -->
|
||||
filter(db.root.mediaFiles.contains(it)).
|
||||
map(db.root.mediaFiles[it])
|
||||
|
||||
proc findMediaFilesByTag*(db: WdiwtltDb, tag: Tag): seq[MediaFile] =
|
||||
return db.root.tagsAndMediaFiles -->
|
||||
filter(db.root.mediaFiles.contains(it.mediaFileId) and it.tagId == tag.id).
|
||||
map(db.root.mediaFiles[it.mediaFileId])
|
||||
|
||||
proc findAllRelatedMediaFiles(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[MediaFile] =
|
||||
# Note, contrary to other model types, we do not deduplicate the media file
|
||||
# lookup as the expected behavior when listing by multiple
|
||||
# albums/artists/etc. is to get all the media files for the model in order.
|
||||
# In general there should be duplication unless the exact same media file is
|
||||
# present in multiple artist/album/etc. collections. This would need to be
|
||||
# done intentionally, as the current scan implementation has no mechanism for
|
||||
# creating this kind of duplicate link (being path-based).
|
||||
|
||||
if sourceModels.len == 0: return @[]
|
||||
let firstModel = sourceModels[0]
|
||||
|
||||
if firstModel of Album:
|
||||
return sourceModels -->
|
||||
map(db.findMediaFilesByAlbum(it.Album)).flatten()
|
||||
|
||||
elif firstModel of Artist:
|
||||
return sourceModels -->
|
||||
map(db.findMediaFilesByArtist(it.Artist)).flatten()
|
||||
|
||||
elif firstModel of Bookmark:
|
||||
return sourceModels -->
|
||||
map(db.findMediaFilesByBookmark(it.Bookmark)).flatten()
|
||||
|
||||
elif firstModel of MediaFile:
|
||||
return sourceModels --> map(it.MediaFile)
|
||||
|
||||
elif firstModel of Playlist:
|
||||
return sourceModels -->
|
||||
map(db.findMediaFilesByPlaylist(it.Playlist)).flatten()
|
||||
|
||||
elif firstModel of Tag:
|
||||
return sourceModels -->
|
||||
map(db.findMediaFilesByTag(it.Tag)).flatten()
|
||||
|
||||
else:
|
||||
warn "unknown source model type, returning empty selection"
|
||||
debug "findAllRelatedMediaFiles: first source model was " & $firstModel
|
||||
|
||||
## Playlists
|
||||
## --------------------
|
||||
proc add*(db: WdiwtltDb, p: Playlist) = db.root.playlists[p.id] = p
|
||||
@ -410,16 +688,150 @@ proc remove*(db: WdiwtltDb, p: Playlist) =
|
||||
|
||||
proc update*(db: WdiwtltDb, p: Playlist) = db.add(p)
|
||||
|
||||
proc allPlaylists*(db: WdiwtltDb): seq[Playlist] =
|
||||
db.root.playlists.values.toSeq
|
||||
|
||||
proc findPlaylistById*(db: WdiwtltDb, id: UUID): Option[Playlist] =
|
||||
if not db.root.playlists.contains(id): return none[Playlist]()
|
||||
return some(db.root.playlists[id])
|
||||
|
||||
proc findPlaylistsByMediaFile*(db: WdiwtltDb, mf: MediaFile): seq[Playlist] =
|
||||
db.root.playlistsToMediaFiles.pairs.toSeq -->
|
||||
filter(db.root.playlists.contains(it[0]) and it[1].contains(mf.id)).
|
||||
map(db.root.playlists[it[0]])
|
||||
|
||||
proc findPlaylistsByName*(db: WdiwtltDb, name: string): seq[Playlist] =
|
||||
db.root.playlists.values.toSeq --> filter(it.name == name)
|
||||
|
||||
proc findAllRelatedPlaylists(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Playlist] =
|
||||
|
||||
if sourceModels.len == 0: return @[]
|
||||
let firstModel = sourceModels[0]
|
||||
|
||||
if firstModel of Bookmark:
|
||||
return sourceModels -->
|
||||
map(it.Bookmark).
|
||||
filter(db.root.playlists.contains(it.playlistId)).
|
||||
map(db.root.playlists[it.playlistId])
|
||||
|
||||
elif firstModel of Playlist:
|
||||
return sourceModels --> map(it.Playlist)
|
||||
|
||||
elif firstModel of Album or
|
||||
firstModel of Artist or
|
||||
firstModel of MediaFile or
|
||||
firstModel of Tag:
|
||||
|
||||
return deduplicate(
|
||||
db.findAllRelatedMediaFiles(sourceModels) -->
|
||||
map(db.findPlaylistsByMediaFile(it)).flatten())
|
||||
|
||||
else:
|
||||
warn "unknown source model type, returning empty selection"
|
||||
debug "findAllRelatedPlaylists: first source model was " & $firstModel
|
||||
|
||||
## Tags
|
||||
## --------------------
|
||||
proc add*(db: WdiwtltDb, t: Tag) = db.root.tags[t.name] = t.description
|
||||
proc add*(db: WdiwtltDb, t: Tag) =
|
||||
db.root.tags[t.id] = t
|
||||
db.root.tagSet.incl(t.name)
|
||||
|
||||
proc remove*(db: WdiwtltDb, t: Tag) =
|
||||
db.root.tags.del(t.name)
|
||||
db.root.tags.del(t.id)
|
||||
db.root.tagSet.excl(t.name)
|
||||
db.root.tagsAndMediaFiles = db.root.tagsAndMediaFiles -->
|
||||
filter(it.tagName != t.name)
|
||||
filter(it.tagId != t.id)
|
||||
|
||||
proc update*(db: WdiwtltDb, t: Tag) = db.add(t)
|
||||
proc update*(db: WdiwtltDb, t: Tag) =
|
||||
if db.root.tags.contains(t.id):
|
||||
db.root.tagSet.excl(db.root.tags[t.id].name)
|
||||
|
||||
db.add(t)
|
||||
|
||||
proc allTags*(db: WdiwtltDb): seq[Tag] =
|
||||
db.root.tags.values.toSeq
|
||||
|
||||
proc findTagById*(db: WdiwtltDb, id: UUID): Option[Tag] =
|
||||
if not db.root.tags.contains(id): return none[Tag]()
|
||||
return some(db.root.tags[id])
|
||||
|
||||
proc findTagByName*(db: WdiwtltDb, name: string): Option[Tag] =
|
||||
if not db.root.tagSet.contains(name): return none[Tag]()
|
||||
let foundTags = db.root.tags.values.toSeq --> filter(it.name == name)
|
||||
if foundTags.len == 0: return none[Tag]()
|
||||
else: return some(foundTags[0])
|
||||
|
||||
proc findTagsByMediaFile*(db: WdiwtltDb, mf: MediaFile): seq[Tag] =
|
||||
db.root.tagsAndMediaFiles -->
|
||||
filter(it.mediaFileId == mf.id).
|
||||
map(db.root.tags[it.tagId])
|
||||
|
||||
proc findAllRelatedTags(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel]
|
||||
): seq[Tag] =
|
||||
|
||||
if sourceModels.len == 0: return @[]
|
||||
let firstModel = sourceModels[0]
|
||||
|
||||
if firstModel of Tag:
|
||||
return sourceModels --> map(it.Tag)
|
||||
|
||||
elif firstModel of Album or
|
||||
firstModel of Artist or
|
||||
firstModel of Bookmark or
|
||||
firstModel of MediaFile or
|
||||
firstModel of Playlist:
|
||||
|
||||
return deduplicate(
|
||||
db.findAllRelatedMediaFiles(sourceModels) -->
|
||||
map(db.findTagsByMediaFile(it)).flatten())
|
||||
|
||||
else:
|
||||
warn "unknown source model type, returning empty selection"
|
||||
|
||||
## General
|
||||
## --------------------
|
||||
|
||||
proc getAll*(db: WdiwtltDb, mt: ModelType): seq[BaseModel] =
|
||||
case mt:
|
||||
of mtAlbum: db.root.albums.values.toSeq --> map(it.BaseModel)
|
||||
of mtArtist: db.root.artists.values.toSeq --> map(it.BaseModel)
|
||||
of mtBookmark: db.root.bookmarks.values.toSeq --> map(it.BaseModel)
|
||||
of mtMediaFile: db.root.bookmarks.values.toSeq --> map(it.BaseModel)
|
||||
of mtPlaylist: db.root.playlists.values.toSeq --> map(it.BaseModel)
|
||||
of mtTag: db.root.tags.values.toSeq --> map(it.BaseModel)
|
||||
|
||||
|
||||
proc findAllRelatedModels*(
|
||||
db: WdiwtltDb,
|
||||
sourceModels: seq[BaseModel],
|
||||
targetModelType: ModelType
|
||||
): seq[BaseModel] =
|
||||
|
||||
if sourceModels.len <= 0: return @[]
|
||||
|
||||
case targetModelType:
|
||||
of mtAlbum:
|
||||
return db.findAllRelatedAlbums(sourceModels) --> map(it.BaseModel)
|
||||
|
||||
of mtArtist:
|
||||
return db.findAllRelatedArtists(sourceModels) --> map(it.BaseModel)
|
||||
|
||||
of mtBookmark:
|
||||
return db.findAllRelatedBookmarks(sourceModels) --> map(it.BaseModel)
|
||||
|
||||
of mtMediaFile:
|
||||
return db.findAllRelatedMediaFiles(sourceModels) --> map(it.BaseModel)
|
||||
|
||||
of mtPlaylist:
|
||||
return db.findAllRelatedPlaylists(sourceModels) --> map(it.BaseModel)
|
||||
|
||||
of mtTag:
|
||||
return db.findAllRelatedTags(sourceModels) --> map(it.BaseModel)
|
||||
|
||||
## Housekeeping
|
||||
## --------------------
|
||||
|
40
src/main/nim/wdiwtlt/incremental_md5.nim
Normal file
40
src/main/nim/wdiwtlt/incremental_md5.nim
Normal file
@ -0,0 +1,40 @@
|
||||
import md5, streams
|
||||
import os
|
||||
|
||||
proc fileToMD5*(filename: string) : string =
|
||||
|
||||
const blockSize: int = 8192 # read files in 8KB chunnks
|
||||
var
|
||||
c: MD5Context
|
||||
d: MD5Digest
|
||||
fs: FileStream
|
||||
buffer: string
|
||||
|
||||
#read chunk of file, calling update until all bytes have been read
|
||||
try:
|
||||
fs = filename.open.newFileStream
|
||||
|
||||
md5Init(c)
|
||||
buffer = fs.readStr(blockSize)
|
||||
|
||||
while buffer.len > 0:
|
||||
md5Update(c, buffer, buffer.len)
|
||||
buffer = fs.readStr(blockSize)
|
||||
|
||||
md5Final(c, d)
|
||||
|
||||
except IOError: echo("File not found.")
|
||||
finally:
|
||||
if fs != nil:
|
||||
close(fs)
|
||||
|
||||
result = $d
|
||||
|
||||
when isMainModule:
|
||||
|
||||
if paramCount() > 0:
|
||||
let arguments = commandLineParams()
|
||||
echo("MD5: ", fileToMD5(arguments[0]))
|
||||
else:
|
||||
echo("Must pass filename.")
|
||||
quit(-1)
|
@ -1,13 +1,13 @@
|
||||
import std/[logging, nre, options, os, times, unicode]
|
||||
import std/[logging, nre, options, os, sets, times, unicode]
|
||||
import std/strutils except strip
|
||||
import console_progress, uuids
|
||||
import console_progress, uuids, zero_functional
|
||||
import ./db, ./incremental_md5, ./libvlc, ./models
|
||||
|
||||
type
|
||||
WdiwtltLibrary* = ref object
|
||||
rootPath: string
|
||||
#autoDeletePeriodMs*: int # 1000 * 60 * 60 * 24 * 6 # one week
|
||||
db: WdiwtltDb
|
||||
db*: WdiwtltDb
|
||||
vlc: LibVlcInstance
|
||||
|
||||
let FILENAME_PAT = re"^(\d+)?[:\-_ ]*(.+)$"
|
||||
@ -127,8 +127,9 @@ proc scan*(l: WdiwtltLibrary, fullRescan = false) =
|
||||
var fileCount = 0
|
||||
for f in l.walkMediaFiles: fileCount += 1
|
||||
|
||||
let progress = newProgress(stdout, fileCount)
|
||||
let progress = newProgress(stdout, fileCount + 1)
|
||||
|
||||
var seenMediaFileIds = initHashSet[UUID]()
|
||||
var curCount = 0
|
||||
debug "Scanning media library root at " & l.rootPath
|
||||
for f in l.walkMediaFiles:
|
||||
@ -141,7 +142,14 @@ proc scan*(l: WdiwtltLibrary, fullRescan = false) =
|
||||
var existingMf = l.db.findMediaFileByPath(f)
|
||||
#let hash = fileToMD5(fullfn)
|
||||
#l.db.findMediaFileByHash(hash)
|
||||
if not fullRescan and existingMf.isSome: continue
|
||||
if existingMf.isSome:
|
||||
seenMediaFileIds.incl(existingMf.get.id)
|
||||
# TODO: the implementation that follows will create duplicate
|
||||
# entries when rescanning the library. Until that is fixes, we will not
|
||||
# support recan
|
||||
if not fullRescan:
|
||||
raise newException(CliErr, "rescan is not currently supported")
|
||||
#continue
|
||||
|
||||
# Process this new file
|
||||
let (mf, artistsFromMeta, albumsFromMeta, trackTotal) =
|
||||
@ -192,6 +200,13 @@ proc scan*(l: WdiwtltLibrary, fullRescan = false) =
|
||||
for artist in allArtists:
|
||||
l.db.associate(allArtists[0], allAlbums[0])
|
||||
|
||||
progress.updateProgress(curCount, "")
|
||||
progress.updateProgress(curCount, "Looking for absent media files.")
|
||||
|
||||
l.db.allMediaFiles -->
|
||||
filter(not seenMediaFileIds.contains(it.id)).
|
||||
foreach(it.presentLocally = false)
|
||||
|
||||
progress.updateProgress(curCount, "Looking for absent media files.")
|
||||
|
||||
stdout.writeLine("Scan complete")
|
||||
l.db.persist
|
||||
|
@ -1,10 +1,14 @@
|
||||
import std/[json, jsonutils, options, strutils, times]
|
||||
import timeutils, uuids
|
||||
import ./jsonutils as addtnl_jsonutils
|
||||
|
||||
type
|
||||
MetaSource* = enum msTagInfo = "tag info", msFileLocation = "file location"
|
||||
|
||||
BaseModel* = object of RootObj
|
||||
ModelType* = enum
|
||||
mtAlbum, mtArtist, mtBookmark, mtMediaFile, mtPlaylist, mtTag
|
||||
|
||||
BaseModel* {.inheritable.} = object
|
||||
id*: UUID
|
||||
name*: string
|
||||
|
||||
@ -45,10 +49,33 @@ type
|
||||
createdAt*: DateTime
|
||||
lastUsed*: DateTime
|
||||
|
||||
Tag* = object
|
||||
name*: string
|
||||
Tag* = object of BaseModel
|
||||
description*: Option[string]
|
||||
|
||||
CliErr* = object of CatchableError
|
||||
|
||||
SomeModel* = BaseModel | Album | Artist | Bookmark |
|
||||
MediaFile | Playlist | Tag
|
||||
|
||||
proc mapModelType*(str: string): ModelType =
|
||||
case str.toLower:
|
||||
of "album": return mtAlbum
|
||||
of "artist": return mtArtist
|
||||
of "bookmark": return mtBookmark
|
||||
of "file": return mtMediaFile
|
||||
of "playlist": return mtPlaylist
|
||||
of "tag": return mtTag
|
||||
else: raise newException(CliErr, "unrecognized item type: " & str)
|
||||
|
||||
proc modelMatchesType*(model: BaseModel, mt: ModelType): bool =
|
||||
case mt:
|
||||
of mtAlbum: return model of Album
|
||||
of mtArtist: return model of Artist
|
||||
of mtBookmark: return model of Bookmark
|
||||
of mtMediaFile: return model of MediaFile
|
||||
of mtPlaylist: return model of Playlist
|
||||
of mtTag: return model of Tag
|
||||
|
||||
proc `$`*(mf: MediaFile): string =
|
||||
if mf.trackNumber.isSome: align($mf.trackNumber.get, 2) & " - " & mf.name
|
||||
else: mf.name
|
||||
@ -59,10 +86,6 @@ proc `$`*(a: Album): string =
|
||||
|
||||
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)
|
||||
|
@ -1,3 +1,5 @@
|
||||
import std/strformat
|
||||
|
||||
const WDIWTLT_VERSION* = "1.0.0"
|
||||
|
||||
const USAGE* = "wdiwtlt v" & WDIWTLT_VERSION & """
|
||||
@ -21,9 +23,443 @@ Options:
|
||||
|
||||
The path to the JSON database file.
|
||||
|
||||
--debug
|
||||
--log-file <log-file>
|
||||
|
||||
Enable debug logging.
|
||||
Write debug logs to <log-file>
|
||||
|
||||
--verbose
|
||||
|
||||
Enable verbose debug logging to stdout.
|
||||
"""
|
||||
|
||||
const ONLINE_HELP* = ""
|
||||
const FMT_MARK* = "∙"
|
||||
|
||||
const FC* = FMT_MARK & "COMMAND" & FMT_MARK
|
||||
const FE* = FMT_MARK & "ERROR" & FMT_MARK
|
||||
const FN* = FMT_MARK & "NORMAL" & FMT_MARK
|
||||
|
||||
const HELP_GENERAL* = fmt("""Available Commands:
|
||||
|
||||
scan Scan the media library for new files.
|
||||
rescan Re-scan the media library, new and existing files.
|
||||
quit Quit this program.
|
||||
Aliases: {FC}exit{FN}
|
||||
|
||||
A new user is advised to read the help section for the {FC}select{FN} command.
|
||||
|
||||
For a quick list of options, consider reading the {FC}summary{FN} section.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_QUIT* = fmt("""
|
||||
{FC}quit{FN} Close the wdiwtlt program.
|
||||
{FC}exit{FN}
|
||||
|
||||
""")
|
||||
|
||||
const HELP_SCAN* = fmt("""
|
||||
{FC}scan{FN}
|
||||
|
||||
Scan the media library for new files. This will skip over files that the
|
||||
library is already aware of.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_RESCAN* = fmt("""
|
||||
{FC}rescan{FN}
|
||||
|
||||
Scan the media library, processing all files in the library directory,
|
||||
including files we have already scanned previously.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_LIST* = fmt("""
|
||||
{FC}list bookmarks{FN} List all bookmarks.
|
||||
{FC}list selection{FN} List the currently selected items.
|
||||
{FC}list <select-criteria>{FN} Make a selection using the {FC}select{FN} syntax and then
|
||||
list the selection.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_SELECT* = fmt("""
|
||||
{FC}select playing {{ album | artist | file | playlist | tag }}{FN}
|
||||
|
||||
Select the currently playing items into the selection buffer. Specifically,
|
||||
this selects items that are associated with the currently playing media
|
||||
file.
|
||||
|
||||
{FC}select queued {{ albums | artists | files | playlists | tags }}{FN}
|
||||
|
||||
Select the items currently in the queue.
|
||||
|
||||
{FC}select <count> random {{ albums | artists | files | playlists | tags }}{FN}
|
||||
|
||||
Select one or more items randomly.
|
||||
|
||||
{FC}select <count> random {{ albums | artists | files | playlists | tags }} from <select-criteria>{FN}
|
||||
|
||||
Make a selection, then select one or more items randomly from it.
|
||||
|
||||
{FC}select selected {{ album | artist | file | playlist | tag }}{FN}
|
||||
|
||||
Select the items associated with the current selection buffer into the
|
||||
selection buffer. This is useful to change the type of the selection. For
|
||||
example, the following commands would select all of the albums in the
|
||||
library by Bob Dylan:
|
||||
|
||||
select artist Bob Dylan
|
||||
select selected albums
|
||||
|
||||
Another example, to select media files based on a set of tags:
|
||||
|
||||
select tags instrumental orchestral
|
||||
select selected files
|
||||
|
||||
This example would select all files that are tagged as *either*
|
||||
instrumental or orchestral
|
||||
|
||||
{FC}select absent files{FN}
|
||||
|
||||
Select al media files which have entries in the database but are not
|
||||
actually present locally on disk.
|
||||
|
||||
{FC}select files tagged as <tag-name>...{FN}
|
||||
|
||||
Select all media files tagged with the given tags. If multiple tags are
|
||||
given then only files which have all the given tags are selected. In
|
||||
contrast to the previous example:
|
||||
|
||||
select files tagged as instrumental orchestral
|
||||
|
||||
This selects all files that are tagged as *both* instrumental and
|
||||
orchestral.
|
||||
|
||||
{FC}select untagged files{FN}
|
||||
|
||||
Select all media files that do not have any tags associated with them.
|
||||
|
||||
|
||||
{FC}select {{ album | artist | file | playlist | tag }} where <criteria>...{FN}
|
||||
|
||||
{FE}Not yet implemented.{FN}
|
||||
|
||||
{FC}select {{ album | artist | file | playlist | tag }} <id | name>{FN}
|
||||
|
||||
Select a single item by ID or by name. When selecting by name, the name can
|
||||
include spaces and can be a substring of the whole name ("Lonely Hearts"
|
||||
for "Sgt. Pepper's Lonely Hearts Club Band" for example, quotations not
|
||||
required).
|
||||
|
||||
{FC}select {{ albums | artists | files | playlists | tags }} <id>...{FN}
|
||||
|
||||
Select multiple items by ID. Multiple IDs can be given, separated by
|
||||
spaces. If no IDs are given, all of the items are returned.
|
||||
|
||||
{FC}queue{FN}
|
||||
|
||||
Select the current play queue
|
||||
|
||||
""")
|
||||
|
||||
const HELP_CREATE* = fmt("""
|
||||
{FC}create bookmark named <name>{FN}
|
||||
|
||||
Create a new bookmark at the current play position in the currently playing
|
||||
playlist.
|
||||
|
||||
{FC}create bookmark named <name> on playlist <playlist-name | id> at <file-name | id>{FN}
|
||||
|
||||
Create a bookmark on the named media file in the named playlist.
|
||||
|
||||
{FC}create playlist named <name>{FN}
|
||||
|
||||
Create a new playlist.
|
||||
|
||||
{FC}create playlist named <name> from {{ queue | selection}}{FN}
|
||||
|
||||
Create a new playlist and populate it with the contents of either the
|
||||
current play queue or the current selection.
|
||||
""")
|
||||
|
||||
const HELP_DELETE* = fmt("""
|
||||
{FC}delete playlist <name | id>
|
||||
delete bookmark <name | id>{FN}
|
||||
|
||||
Delete a playlist or bookmark.
|
||||
""")
|
||||
|
||||
const HELP_PLAY* = fmt("""
|
||||
{FC}play{FN}
|
||||
|
||||
With no options, play the current file (inverse of pause).
|
||||
|
||||
{FC}play selection{FN}
|
||||
|
||||
Clear the play queue, enqueue the current selection, and begin playback.
|
||||
|
||||
{FC}play bookmark <id | name>{FN}
|
||||
|
||||
Load the bookmarked playlist as the play queue and begin playback at the
|
||||
bookmarked media file.
|
||||
|
||||
{FC}play <select-criteria>{FN}
|
||||
|
||||
Make a selection using the {FC}select{FN} syntax and then play the selection.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_ENQUEUE* = fmt("""
|
||||
{FC}enqueue selection{FN}
|
||||
|
||||
Add the media files for the selected items to the end of the current play
|
||||
queue.
|
||||
|
||||
{FC}enqueue <select-criteria>{FN}
|
||||
|
||||
Make a selection using the {FC}select{FN} syntax and then enqueue
|
||||
the selection.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_ADD* = fmt("""
|
||||
{FC}add selection to playlist <id | name>{FN}
|
||||
|
||||
Lookup a playlist by id or name (including partial match) then add the
|
||||
media files for the selected items to the end of that playlist.
|
||||
|
||||
{FC}add <select-criteria> to playlist <id | name>{FN}
|
||||
|
||||
Lookup a playlist by id or name (including partial match), select a set of
|
||||
media files using the {FC}select{FN} syntax, then add the media files
|
||||
for the selected items to the end of that playlist.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_REMOVE* = fmt("""
|
||||
{FC}remove selection from {{ queue | selection }}{FN}
|
||||
{FC}remove selection from playlist <id | name>{FN}
|
||||
|
||||
Remove the media files for the current selection from the current
|
||||
selection, the current play queue, or from a playlist looked up by ID or
|
||||
name (including partial match).
|
||||
|
||||
{FC}remove <select-criteria> from {{ queue | selection }}{FN}
|
||||
{FC}remove <select-criteria> from playlist <id | name>{FN}
|
||||
|
||||
Make a selection using the {FC}select{FN} syntax then remove those media
|
||||
files from either the current selection, the play queue, or from a
|
||||
playlist looked up by ID or name (including partial match).
|
||||
|
||||
""")
|
||||
|
||||
const HELP_RANDOMIZE* = fmt("""
|
||||
{FC}randomize queue{FN}
|
||||
{FC}randomize playlist <id | name>{FN}
|
||||
|
||||
Randomize the order of all elements in either the current play queue or the
|
||||
named playlist.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_REORDER* = fmt("""
|
||||
{FC}reorder queue move <file id | name> to <position>{FN}
|
||||
{FC}reorder playlist <id | name> move <file id | name> to <position>{FN}
|
||||
|
||||
Move a single file from its current position to the named position in the
|
||||
play queue/playlist.
|
||||
|
||||
{FC}reorder queue as <file id>... starting at <position>{FN}
|
||||
{FC}reorder playlist <id | name> as <file id>... starting at <position>{FN}
|
||||
|
||||
Take a set of files in the playlist, reorder them into given order, and
|
||||
move them to the given starting position (defaults to the end of the
|
||||
playlist)
|
||||
|
||||
""")
|
||||
|
||||
const HELP_TAG* = fmt("""
|
||||
{FC}tag <tag>...{FN}
|
||||
|
||||
Tag the currently playing file with the given tags. Multiple tags may be
|
||||
provided, separated by spaces (tags cannot include spaces).
|
||||
|
||||
{FC}tag selection as <tag>...{FN}
|
||||
|
||||
Tag all of the media files in the current selection with the given tags.
|
||||
Multiple tags may be provided separated by spaces (tags cannot include
|
||||
spaces).
|
||||
|
||||
{FC}tag <select-criteria> as <tag>...{FN}
|
||||
|
||||
Make a selection using the {FC}select{FN} syntax then tag all of the
|
||||
media files in the selection with the given tags. Multiple tags may be
|
||||
provided separated by spaces (tags cannot include spaces).
|
||||
|
||||
""")
|
||||
const HELP_UNTAG* = fmt("""
|
||||
{FC}untag <tag>...{FN}
|
||||
|
||||
Remove the given tags from the currently playing file. Multiple tags may be
|
||||
provided, separated by spaces (tags cannot include spaces).
|
||||
|
||||
{FC}untag selection as <tag>...{FN}
|
||||
|
||||
Remove the given tags from all of the media files in the current selection.
|
||||
Multiple tags may be provided separated by spaces (tags cannot include
|
||||
spaces).
|
||||
|
||||
{FC}untag <select-criteria> as <tag>...{FN}
|
||||
|
||||
Make a selection using the {FC}select{FN} syntax then remove the
|
||||
given tags from all of the media files in the selection. Multiple tags may
|
||||
be provided separated by spaces (tags cannot include spaces).
|
||||
|
||||
""")
|
||||
|
||||
const HELP_CLEAR* = fmt("""
|
||||
{FC}clear{FN} Clear the terminal display.
|
||||
{FC}clear queue{FN} Clear the play queue.
|
||||
{FC}clear selection{FN} Clear the selection buffer.
|
||||
{FC}clear playlist <id | name>{FN} Clear the given playlist
|
||||
|
||||
""")
|
||||
const HELP_PAUSE* = "{FC}pause{FN} Pause playback."
|
||||
const HALPE_STOP* = "{FC}stop{FN} Stop playback."
|
||||
|
||||
const HELP_NEXT* = fmt(
|
||||
"""
|
||||
{FC}next <count>{FN} Move forward in the play queue by <count> items. <count> is
|
||||
optional and defaults to 1
|
||||
|
||||
""")
|
||||
|
||||
const HELP_PREV* = fmt(
|
||||
"""
|
||||
prev <count> Move backward in the play queue by <count> items. <count> is
|
||||
optional and defaults to 1
|
||||
|
||||
""")
|
||||
|
||||
const HELP_JUMP* = fmt("""
|
||||
{FC}jump to <media file ID or name>{FN}
|
||||
|
||||
Find the given media file by ID or name in the current play queue and
|
||||
resume playback starting from that file.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_FF* = fmt("""
|
||||
{FC}ff <amount> <unit>{FN}
|
||||
|
||||
Jump forward in the playback of the current media by <amount> specified in
|
||||
<unit>s. <amount> must be an integer. <unit> may be one of: 'millisecons',
|
||||
'seconds', or 'minutes'. The following abbreviations are allowed: 'ms',
|
||||
'millis', 's', 'sec', 'm', 'min'.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_RW* = fmt("""
|
||||
{FC}rw <amount> <unit>{FN}
|
||||
|
||||
Jump backward in the playback of the current media by <amount> specified in
|
||||
<unit>s. <amount> must be an integer. <unit> may be one of: 'millisecons',
|
||||
'seconds', or 'minutes'. The following abbreviations are allowed: 'ms',
|
||||
'millis', 's', 'sec', 'm', 'min'.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_REPEAT* = fmt("""
|
||||
{FC}repeat {{ all | one | none }}{FN}
|
||||
|
||||
Set the playlist repeat mode to:
|
||||
all: repeat the entire play queue
|
||||
one: loop the currently playing song
|
||||
none: do not repeat.
|
||||
""")
|
||||
|
||||
const HELP_VOL* = fmt(
|
||||
"""
|
||||
volume Display the current volume setting.
|
||||
volume <percent> Set the volume. <percent> may be any value from 0 to 200.
|
||||
|
||||
""")
|
||||
|
||||
const HELP_HELP* = fmt(
|
||||
"""
|
||||
help summary Display a quick summary of available commands.
|
||||
help <commmand> Display detailed information about the given command.""")
|
||||
|
||||
|
||||
const HELP_SUMMARY* = fmt(
|
||||
"""
|
||||
Selecting files:
|
||||
|
||||
select {{ album | artist | file | playlist | tag }} <id | name>
|
||||
select {{ albums | artists | files | playlists | tags }} where <criteria>
|
||||
select files tagged as <tag>... and not as <tag>...
|
||||
select playing {{ albums | artists | files | playlists | tags }}
|
||||
select queued {{ albums | artists | files | playlists | tags }}
|
||||
select absent files
|
||||
|
||||
list selection
|
||||
list <selection-criteria>
|
||||
list selected {{ albums | artists | files | playlists | tags }}
|
||||
|
||||
Play and Controlling Media:
|
||||
|
||||
play
|
||||
play selection
|
||||
play bookmark <id | name>
|
||||
play <selection-criteria>
|
||||
|
||||
next <count> (alias: n)
|
||||
prev <count> (alias: p)
|
||||
|
||||
repeat {{ all | one | off }}
|
||||
stop
|
||||
pause
|
||||
fasforward <duration> <time-unit> (aliases: ff, fwd)
|
||||
rewind <duration> <time-unit> (aliases: rw, rwd)
|
||||
jump <id | name>
|
||||
|
||||
Playlist and Queue Management:
|
||||
|
||||
enqueue selection
|
||||
enqueue <selection-criteria>
|
||||
|
||||
add selection to playlist to playlist <id | name>
|
||||
add <selection-criteria> to playlist <id | name>
|
||||
|
||||
remove selection from playlist <id | name>
|
||||
remove <selection-criteria> from playlist <id | name>
|
||||
remove selection from {{ queue | selection }}
|
||||
remove <selection-criteria> from {{ queue | selection }}
|
||||
|
||||
create bookmark named <name>
|
||||
create bookmark named <name> on playlist <playlist name | id> at <file name | id>
|
||||
create playlist named <name> from {{ selection | queue }}
|
||||
create playlist named <name from <selection-criteria>
|
||||
|
||||
copy playlist <id | name> as <new name>
|
||||
create bookmark <id | name>
|
||||
update bookmark <id | name>
|
||||
|
||||
delete playlist <id | name>
|
||||
delete bookmark <id | name>
|
||||
|
||||
randomize {{ queue | playlist | selection }}
|
||||
|
||||
reorder queue move <file id | name> to <position>
|
||||
reorder playlist <id | name> move <file id | name> to <position>
|
||||
reorder queue as <file id>...
|
||||
reorder playlist <id | name> as <file id>...
|
||||
|
||||
clear
|
||||
clear queue
|
||||
clear selection
|
||||
clear playlist <id | name>
|
||||
|
||||
Library Management:
|
||||
|
||||
scan
|
||||
""")
|
||||
|
99
src/main/nim/wdiwtlt/private/styledtext.nim
Normal file
99
src/main/nim/wdiwtlt/private/styledtext.nim
Normal file
@ -0,0 +1,99 @@
|
||||
import std/[logging, strutils, tables, terminal]
|
||||
import ./cliconstants
|
||||
|
||||
type
|
||||
TextStyle* = enum
|
||||
tsAlbum, tsArtist, tsCommand, tsError, tsFile, tsNormal, tsOption,
|
||||
tsPlaylist, tsPrompt, tsStatus, tsTitle,
|
||||
|
||||
StyledText* = object
|
||||
style: TextStyle
|
||||
text: string
|
||||
|
||||
let styleToColorMap = newTable[TextStyle, ForegroundColor]([
|
||||
(tsAlbum, fgBlue),
|
||||
(tsArtist, fgRed),
|
||||
(tsCommand, fgYellow),
|
||||
(tsError, fgRed),
|
||||
(tsFile, fgGreen),
|
||||
(tsNormal, fgWhite),
|
||||
(tsOption, fgGreen),
|
||||
(tsPlaylist, fgGreen),
|
||||
(tsPrompt, fgYellow),
|
||||
(tsStatus, fgCyan),
|
||||
(tsTitle, fgWhite)
|
||||
])
|
||||
|
||||
let styleToFormatMap = newTable[TextStyle, set[Style]]([
|
||||
(tsAlbum, {}),
|
||||
(tsArtist, {}),
|
||||
(tsCommand, {styleBright}),
|
||||
(tsError, {styleBright}),
|
||||
(tsFile, {}),
|
||||
(tsNormal, {}),
|
||||
(tsOption, {styleBright}),
|
||||
(tsPlaylist, {}),
|
||||
(tsPrompt, {styleBright}),
|
||||
(tsStatus, {}),
|
||||
(tsTitle, {styleBright})
|
||||
])
|
||||
|
||||
func parseStyledText*(text: string): seq[StyledText] =
|
||||
result = newseq[StyledText]()
|
||||
var curIdx = 0
|
||||
var curStyle = tsNormal
|
||||
|
||||
while curIdx < text.len:
|
||||
let nextMarkerIdx = text.find(FMT_MARK, curIdx)
|
||||
|
||||
if nextMarkerIdx == -1:
|
||||
result.add(StyledText(
|
||||
style: curStyle,
|
||||
text: text[curIdx..^1]))
|
||||
curIdx = text.len
|
||||
|
||||
else:
|
||||
let endMarkerIdx = text.find(FMT_MARK, nextMarkerIdx + FMT_MARK.len)
|
||||
if endMarkerIdx == -1:
|
||||
raise newException(Exception,
|
||||
"invalid text formatting, missing ending marker")
|
||||
|
||||
result.add(StyledText(
|
||||
style: curStyle,
|
||||
text: text[curIdx..<nextMarkerIdx]))
|
||||
|
||||
let styleName = text[(nextMarkerIdx+FMT_MARK.len)..<endMarkerIdx]
|
||||
case styleName:
|
||||
of "NORMAL": curStyle = tsNormal
|
||||
of "COMMAND": curStyle = tsCommand
|
||||
of "ALBUM": curStyle = tsAlbum
|
||||
of "ARTIST": curStyle = tsArtist
|
||||
of "ERROR": curStyle = tsError
|
||||
of "FILE": curStyle = tsFile
|
||||
of "OPTION": curStyle = tsOption
|
||||
of "PLAYLIST": curStyle = tsPlaylist
|
||||
of "PROMPT": curStyle = tsPrompt
|
||||
of "STATUS": curStyle = tsStatus
|
||||
of "TITLE": curStyle = tsTitle
|
||||
else: curStyle = tsNormal
|
||||
|
||||
curIdx = endMarkerIdx + FMT_MARK.len
|
||||
|
||||
proc writeStyledText*(file: File, text: string) =
|
||||
let parsed = parseStyledText(text)
|
||||
|
||||
for p in parsed:
|
||||
if styleToColorMap.contains(p.style) and
|
||||
styleToFormatMap.contains(p.style):
|
||||
|
||||
file.styledWrite(
|
||||
styleToColorMap[p.style], styleToFormatMap[p.style], p.text)
|
||||
|
||||
else:
|
||||
warn "Missing text styling for the " & $p.style & " style."
|
||||
file.styledWrite(p.text)
|
||||
|
||||
|
||||
proc writeStyledTextLine*(file: File, text: string) =
|
||||
file.writeStyledText(text)
|
||||
file.writeLine ""
|
7
src/main/nim/wdiwtlt/whenmatched.nim
Normal file
7
src/main/nim/wdiwtlt/whenmatched.nim
Normal file
@ -0,0 +1,7 @@
|
||||
import nre
|
||||
|
||||
template whenMatched*(str: string, pattern: Regex, body: untyped): untyped =
|
||||
let m = str.match(pattern)
|
||||
if m.isSome:
|
||||
let match {.inject.} = m.get
|
||||
body
|
Loading…
Reference in New Issue
Block a user