Implementing artist/album lookup in CLI.

This commit is contained in:
Jonathan Bernard
2016-02-10 09:53:12 -06:00
parent 6e6defe544
commit 90a11569da
6 changed files with 377 additions and 152 deletions

View File

@ -11,8 +11,8 @@ dependencies {
compile 'ch.qos.logback:logback-core:1.1.3'
compile 'org.slf4j:slf4j-api:1.7.14'
compile 'com.offbytwo:docopt:0.6.0.20150202'
compile 'com.jdbernard:jdb-util:4.2'
compile 'org.fusesource.jansi:jansi-project:1.11'
compile 'com.jdbernard:jdb-util:4.3'
compile 'jline:jline:2.12'
compile project(":wdiwtlt-core")
testCompile 'junit:junit:4.12'

View File

@ -3,11 +3,16 @@ package com.jdbernard.wdiwtlt.cli
import com.jdbernard.wdiwtlt.ConfigWrapper
import com.jdbernard.wdiwtlt.MediaLibrary
import com.jdbernard.wdiwtlt.db.ORM
import com.jdbernard.wdiwtlt.db.models.*
import com.jdbernard.io.NonBlockingInputStreamReader
import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import org.docopt.Docopt
import jline.console.ConsoleReader
import java.util.regex.Matcher
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import static com.jdbernard.util.AnsiEscapeCodeSequence.*
@ -44,33 +49,46 @@ Options:
Configuration:
"""
private static Logger logger =
LoggerFactory.getLogger(CommandLineInterface)
private Properties cliConfig
private MediaLibrary library
private String[] linesBuffer = new String[1024]
private StringBuilder currentLine = new StringBuilder()
private int nextAvailableLineIdx = 0
private int newLineIdx = 0
private String selectedLineIdx = 0
private InputStream sin
private InputStream inStream
private OutputStream outStream
private PrintStream out
private ConsoleReader reader
private Thread consoleReaderThread
private List<String> consoleReadBuffer =
Collections.synchronizedList(new ArrayList<String>())
private synchronized boolean running
private boolean running
private String titleStyle, normalStyle, promptStyle, errorStyle
private String clearRemainingLine = new ANSI().eraseLine(Erase.ToEnd).toString()
private String titleStyle, normalStyle, statusStyle, promptStyle,
artistStyle, albumStyle, fileStyle, errorStyle
private String clearLine = new ANSI().eraseLine(Erase.All).toString()
private String afterInput =
new ANSI().eraseLine(Erase.All).scrollUp().cursorUp().toString()
private String beforeLeader =
new ANSI().saveCursor().cursorPrevLine(2).toString()
private String afterLeader =
new ANSI().restoreCursor().eraseLine(Erase.ToEnd).toString()
private String eraseLeader =
new ANSI().cursorPrevLine().eraseLine(Erase.All).cursorPrevLine().eraseLine(Erase.All).toString()
private String errorMsg = ""
private int lastPromptLineCount = 0
private int displayWidth = 79
private long msgTimeout
private ScrollText currentlyPlaying = new ScrollText(
maxWidth: displayWidth - 15 - VERSION.size(),
text: "No media currently playing.")
private ScrollText status = new ScrollText(maxWidth: displayWidth)
private Date dismissMsgDate = new Date()
def selection = [:]
public static void main(String[] args) {
def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args)
println opts
def exitErr = { msg ->
System.out.err.println("wdiwtlt: $msg")
System.exit(1) }
@ -148,164 +166,248 @@ Configuration:
}
public CommandLineInterface(MediaLibrary library, InputStream sin,
PrintStream out, Properties cliConfig) {
OutputStream out, Properties cliConfig) {
this.cliConfig = cliConfig
this.library = library
this.out = out
this.sin = sin
this.inStream = sin
this.outStream = out
this.msgTimeout = cliConfig["message.timeout"] ?: 5000l
setupTextStyles()
this.consoleReaderThread = new Thread().start(this.&runReaderThread)
}
void setupTextStyles() {
titleStyle = new ANSI().color(Colors.BLUE, Colors.DEFAULT, true).toString()
titleStyle = new ANSI().color(Colors.GREEN, Colors.DEFAULT, false).toString()
normalStyle = new ANSI().resetText().toString()
promptStyle = new ANSI().color(Colors.YELLOW, Colors.DEFAULT, true).toString()
statusStyle = new ANSI().color(Colors.CYAN, Colors.DEFAULT, false).toString()
artistStyle = new ANSI().color(Colors.RED, Colors.DEFAULT, false).toString()
albumStyle = new ANSI().color(Colors.BLUE, Colors.DEFAULT, false).toString()
fileStyle = new ANSI().color(Colors.GREEN, Colors.DEFAULT, false).toString()
errorStyle = new ANSI().color(Colors.RED, Colors.DEFAULT, true).toString()
}
private void runReaderThread() {
this.reader = new ConsoleReader(this.inStream, this.outStream)
this.reader.setPrompt(promptStyle + "> " + normalStyle)
String line
while(this.running && !Thread.currentThread().isInterrupted()) {
line = reader.readLine()
if (line == null || line == "quit" || line == "exit") running = false
else {
consoleReadBuffer.add(line)
outStream.print(afterInput)
outStream.flush() } } }
public void repl() {
this.running = true
String line = null
printPrompt()
outStream.println ""
outStream.println ""
drawLeader()
while(this.running) {
line = readInputLine()
if (line != null) {
out.flush()
errorMsg = ""
if (new Date() > dismissMsgDate) resetStatus()
if (consoleReadBuffer.size() > 0) {
line = consoleReadBuffer.remove(0)
switch(line) {
case ~/quit|exit|\u0004/:
case ~/quit|exit|.+\u0004$/:
running = false
consoleReaderThread.interrupt()
break
case 'scan':
scanMediaLibrary()
break
case 'debug':
outStream.println(
"\n\nConfig: \n" +
cliConfig.collect { "\t${it.key}: ${it.value}" }
.join("\n") +
"\n\n\n")
drawLeader(true)
break
case "artist":
selection.artist = null
resetStatus()
break
case ~/artist (.+)/:
def input = Matcher.lastMatcher[0][1]?.trim()
def match = library.getByIdOrName(input, Artist)
if (!match) setErr("No artist matches '$input'.")
else if (match.size() > 1)
setErr("Multiple artists match '$input': " +
match.collect { "${it.id}: ${it.name}" }
.join(", "))
else {
selection.artist = match[0]
resetStatus() }
break
case "album":
selection.album = null
resetStatus()
break
case ~/album (.+)/:
def input = Matcher.lastMatcher[0][1]?.trim()
def match = library.getByIdOrName(input, Album)
if (!match) setErr("No album matches '$input'.")
else if (match.size() > 1)
setErr("Multiple albums match '$input': " +
match.collect { "${it.id}: ${it.name}" }
.join(", "))
else {
selection.album = match[0]
resetStatus() }
break
case "list artists for album":
if (!selection.album) {
setErr("No album is selected.")
break }
outStream.println(makeArtistList(
library.getArtistsByAlbumId(selection.album.id),
selection.album))
drawLeader(true)
break
case ~/list artists( .+)?/:
def name = Matcher.lastMatcher[0][1]?.trim()
def artists
if (name) artists = library.getArtistsByName(name)
else artists = library.getArtists()
outStream.println(makeArtistList(artists))
drawLeader(true)
break
case "list albums for artist":
if (!selection.artist) {
setErr("No artist selected.")
break }
outStream.println(makeAlbumList(
library.getAlbumsByArtistId(selection.artist.id),
selection.artist))
drawLeader(true)
break
case ~/list albums( .+)?/:
def name = Matcher.lastMatcher[0][1]?.trim()
def albums
if (name) albums = library.getAlbumsByName(name)
else albums = library.getAlbums()
outStream.println(makeAlbumList(albums))
drawLeader(true)
break
default:
errorMsg = "Unrecognized command: '$line'"
Thread.sleep(200)
status.text = errorStyle +
"Unrecognized command: '$line'${normalStyle}"
dismissMsgDate = new Date(new Date().time + msgTimeout)
drawLeader()
Thread.sleep(250)
break
}
printPrompt(true)
} else {
printPrompt(true)
Thread.sleep(200)
drawLeader()
Thread.sleep(250)
}
}
}
private void printPrompt(boolean redraw = false) {
StringBuilder prompt = new StringBuilder()
private void scanMediaLibrary() {
status.text = "Scanning media library..."
library.rescanLibrary()
status.text = "Scanned ? files."
dismissMsgDate = new Date(new Date().time + msgTimeout) }
if (redraw) prompt.append(new ANSI()
.saveCursor()
.cursorPrevLine(lastPromptLineCount))
private void drawLeader(afterOutput = false) {
String leader = beforeLeader + getLeader() +
(afterOutput ?
(promptStyle + "> " + normalStyle) :
afterLeader)
outStream.print(leader)
outStream.flush() }
private String getLeader() {
StringBuilder leader = new StringBuilder()
.append(clearLine)
prompt.append(titleStyle)
.append(titleStyle)
.append("WDIWTLT - v")
.append(VERSION)
.append("-- ")
.append(normalStyle)
.append("TODO\n")
if (errorMsg) prompt
.append(" -- ")
.append(statusStyle)
.append(currentlyPlaying)
.append("\n")
.append(clearLine)
.append(errorStyle)
.append(errorMsg)
.append(normalStyle)
.append(status)
.append("\n")
if (redraw) prompt.append(new ANSI().restoreCursor())
else prompt.append(promptStyle)
.append("> ")
return leader.toString() }
private String setErr(String errMsg) {
status.text = errorStyle + errMsg
dismissMsgDate = new Date(new Date().time + msgTimeout) }
private String makeAlbumList(def albums, Artist artist = null) {
def result = new StringBuilder()
.append(eraseLeader)
.append("--------------------\nAlbums")
if (artist) result.append(" (for artist '$artist'):\n")
else result.append(":\n\n")
result.append(albums.collect { "${it.id}: ${it}" }.join("\n"))
.append("\n\n\n")
return result.toString() }
private String makeArtistList(def artists, Album album = null) {
def result = new StringBuilder()
.append(eraseLeader)
.append("--------------------\nArists")
if (album) result.append(" (for album '$album'):\n")
else result.append(":\n\n")
result.append(artists.collect { "${it.id}: ${it.name}" }.join("\n"))
.append("\n\n\n")
return result.toString() }
private String resetStatus() {
StringBuilder s = new StringBuilder()
if (selection.artist) s.append(artistStyle)
.append(selection.artist)
.append(normalStyle)
.append(" / ")
if (selection.album) s.append(albumStyle)
.append(selection.album)
.append(normalStyle)
.append(" / ")
lastPromptLineCount = 1 + (errorMsg ? 1 : 0)
if (selection.mediaFile) s.append(fileStyle)
.append(selection.mediaFile)
out.print(prompt.toString())
out.flush()
}
if (s.size() == 0) status.text = "No current media selections."
else status.text = s.toString()
private void replaceCurrentLine(String newLine) {
String toPrint = new StringBuilder()
.append(new ANSI().cursorHorizontalAbsolute(3))
.append(clearRemainingLine)
.append(newLine)
.toString()
currentLine = new StringBuilder(newLine)
out.print(toPrint)
out.flush()
}
private String readInputLine() {
int bytesAvailable = sin.available()
if (bytesAvailable) {
byte[] input = new byte[bytesAvailable]
sin.read(input)
int idx = 0
while (idx < input.length) {
switch (input[idx]) {
case 0x08: // backspace
case 0x0B: // enter
linesBuffer[newLineIdx] = currentLine.toString()
newLineIdx = (newLineIdx + 1) % linesBuffer.length
linesBuffer[newLineIdx] = null
replaceCurrentLine("")
break
case 0x1B: // escape (prefix to arrows, etc)
// arrows
if (input.length > idx + 2 && input[idx + 1] == 0x5B) {
switch(input[idx + 2]) {
case 0x41: // up
int ni = selectedLineIdx
ni = (ni - 1 + linesBuffer.length) %
linesBuffer.length
if (linesBuffer[ni] != null) {
selectedLineIdx = ni
replaceCurrentLine(linesBuffer[ni]) }
else {
out.print(new ANSI().cursorBackwards(4) +
clearRemainingLine)
out.flush() }
break
case 0x42: // down
int ni = selectedLineIdx
ni = (ni + 1) % linesBuffer.length
if (linesBuffer[ni] != null) {
selectedLineIdx = ni
replaceCurrentLine(linesBuffer[ni]) }
else {
out.print(new ANSI().cursorBackwards(4) +
clearRemainingLine)
out.flush() }
break
case 0x43: case 0x44: // right & left
out.print(new ANSI().cursorBackwards(4) +
clearRemainingLine)
out.flush()
break
default: break
}
idx += 2
}
else currentLine.append((char)input[idx])
break
default:
currentLine.append((char)input[idx])
break
}
idx++
}
}
if (linesBuffer[nextAvailableLineIdx] != null) {
String line = linesBuffer[nextAvailableLineIdx]
nextAvailableLineIdx =
(nextAvailableLineIdx + 1) % linesBuffer.length
return line }
else return null
}
return status.text}
}

View File

@ -0,0 +1,31 @@
package com.jdbernard.wdiwtlt.cli
public class ScrollText {
public String text = ""
public int maxWidth
private int scrollIdx
public void setMaxWidth(int max) {
this.maxWidth = Math.max(max, 1) }
public void setText(String t) {
this.text = t
this.scrollIdx = Math.max(0, text.size() - 10) }
public String getNextScroll() {
if (text.size() < maxWidth) return text
else {
scrollIdx = (scrollIdx + 1) % text.size()
int endIdx = Math.min(scrollIdx + maxWidth, text.size())
int wrapIdx = (scrollIdx + maxWidth) % text.size()
// don't need to wrap past the end
if (wrapIdx == endIdx) return text[scrollIdx..<endIdx]
return new StringBuilder()
.append(text[scrollIdx..<endIdx])
.append(" ").append(text[0..<wrapIdx]) } }
public String toString() { return getNextScroll() }
}

View File

@ -0,0 +1,19 @@
import ch.qos.logback.core.*;
import ch.qos.logback.core.encoder.*;
import ch.qos.logback.core.read.*;
import ch.qos.logback.core.rolling.*;
import ch.qos.logback.core.status.*;
import ch.qos.logback.classic.net.*;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
appender("FILE", FileAppender) {
file = "wdiwtlt.log"
append = true
encoder(PatternLayoutEncoder) {
pattern = "%level %logger - %msg%n"
}
}
root(OFF)
logger("com.jdbernard.wdiwtlt", DEBUG, ["FILE"])