Implementing artist/album lookup in CLI.
This commit is contained in:
parent
6e6defe544
commit
90a11569da
@ -11,8 +11,8 @@ dependencies {
|
|||||||
compile 'ch.qos.logback:logback-core:1.1.3'
|
compile 'ch.qos.logback:logback-core:1.1.3'
|
||||||
compile 'org.slf4j:slf4j-api:1.7.14'
|
compile 'org.slf4j:slf4j-api:1.7.14'
|
||||||
compile 'com.offbytwo:docopt:0.6.0.20150202'
|
compile 'com.offbytwo:docopt:0.6.0.20150202'
|
||||||
compile 'com.jdbernard:jdb-util:4.2'
|
compile 'com.jdbernard:jdb-util:4.3'
|
||||||
compile 'org.fusesource.jansi:jansi-project:1.11'
|
compile 'jline:jline:2.12'
|
||||||
compile project(":wdiwtlt-core")
|
compile project(":wdiwtlt-core")
|
||||||
|
|
||||||
testCompile 'junit:junit:4.12'
|
testCompile 'junit:junit:4.12'
|
||||||
|
@ -3,11 +3,16 @@ package com.jdbernard.wdiwtlt.cli
|
|||||||
import com.jdbernard.wdiwtlt.ConfigWrapper
|
import com.jdbernard.wdiwtlt.ConfigWrapper
|
||||||
import com.jdbernard.wdiwtlt.MediaLibrary
|
import com.jdbernard.wdiwtlt.MediaLibrary
|
||||||
import com.jdbernard.wdiwtlt.db.ORM
|
import com.jdbernard.wdiwtlt.db.ORM
|
||||||
|
import com.jdbernard.wdiwtlt.db.models.*
|
||||||
import com.jdbernard.io.NonBlockingInputStreamReader
|
import com.jdbernard.io.NonBlockingInputStreamReader
|
||||||
import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI
|
import com.jdbernard.util.AnsiEscapeCodeSequence as ANSI
|
||||||
import com.zaxxer.hikari.HikariConfig
|
import com.zaxxer.hikari.HikariConfig
|
||||||
import com.zaxxer.hikari.HikariDataSource
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
import org.docopt.Docopt
|
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.*
|
import static com.jdbernard.util.AnsiEscapeCodeSequence.*
|
||||||
|
|
||||||
@ -44,33 +49,46 @@ Options:
|
|||||||
Configuration:
|
Configuration:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
private static Logger logger =
|
||||||
|
LoggerFactory.getLogger(CommandLineInterface)
|
||||||
private Properties cliConfig
|
private Properties cliConfig
|
||||||
private MediaLibrary library
|
private MediaLibrary library
|
||||||
|
|
||||||
private String[] linesBuffer = new String[1024]
|
private InputStream inStream
|
||||||
private StringBuilder currentLine = new StringBuilder()
|
private OutputStream outStream
|
||||||
private int nextAvailableLineIdx = 0
|
|
||||||
private int newLineIdx = 0
|
|
||||||
private String selectedLineIdx = 0
|
|
||||||
private InputStream sin
|
|
||||||
|
|
||||||
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, statusStyle, promptStyle,
|
||||||
|
artistStyle, albumStyle, fileStyle, errorStyle
|
||||||
private String titleStyle, normalStyle, promptStyle, errorStyle
|
|
||||||
private String clearRemainingLine = new ANSI().eraseLine(Erase.ToEnd).toString()
|
|
||||||
private String clearLine = new ANSI().eraseLine(Erase.All).toString()
|
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 displayWidth = 79
|
||||||
private int lastPromptLineCount = 0
|
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) {
|
public static void main(String[] args) {
|
||||||
|
|
||||||
def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args)
|
def opts = new Docopt(DOC).withVersion("wdiwtlt v$VERSION").parse(args)
|
||||||
|
|
||||||
println opts
|
|
||||||
|
|
||||||
def exitErr = { msg ->
|
def exitErr = { msg ->
|
||||||
System.out.err.println("wdiwtlt: $msg")
|
System.out.err.println("wdiwtlt: $msg")
|
||||||
System.exit(1) }
|
System.exit(1) }
|
||||||
@ -148,164 +166,248 @@ Configuration:
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CommandLineInterface(MediaLibrary library, InputStream sin,
|
public CommandLineInterface(MediaLibrary library, InputStream sin,
|
||||||
PrintStream out, Properties cliConfig) {
|
OutputStream out, Properties cliConfig) {
|
||||||
this.cliConfig = cliConfig
|
this.cliConfig = cliConfig
|
||||||
this.library = library
|
this.library = library
|
||||||
this.out = out
|
this.inStream = sin
|
||||||
this.sin = sin
|
this.outStream = out
|
||||||
|
|
||||||
|
this.msgTimeout = cliConfig["message.timeout"] ?: 5000l
|
||||||
|
|
||||||
setupTextStyles()
|
setupTextStyles()
|
||||||
|
|
||||||
|
this.consoleReaderThread = new Thread().start(this.&runReaderThread)
|
||||||
}
|
}
|
||||||
|
|
||||||
void setupTextStyles() {
|
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()
|
normalStyle = new ANSI().resetText().toString()
|
||||||
promptStyle = new ANSI().color(Colors.YELLOW, Colors.DEFAULT, true).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()
|
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() {
|
public void repl() {
|
||||||
this.running = true
|
this.running = true
|
||||||
String line = null
|
String line = null
|
||||||
|
|
||||||
printPrompt()
|
outStream.println ""
|
||||||
|
outStream.println ""
|
||||||
|
drawLeader()
|
||||||
|
|
||||||
while(this.running) {
|
while(this.running) {
|
||||||
|
|
||||||
line = readInputLine()
|
if (new Date() > dismissMsgDate) resetStatus()
|
||||||
if (line != null) {
|
if (consoleReadBuffer.size() > 0) {
|
||||||
out.flush()
|
line = consoleReadBuffer.remove(0)
|
||||||
errorMsg = ""
|
|
||||||
switch(line) {
|
switch(line) {
|
||||||
case ~/quit|exit|\u0004/:
|
case ~/quit|exit|.+\u0004$/:
|
||||||
running = false
|
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
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
errorMsg = "Unrecognized command: '$line'"
|
status.text = errorStyle +
|
||||||
Thread.sleep(200)
|
"Unrecognized command: '$line'${normalStyle}"
|
||||||
|
dismissMsgDate = new Date(new Date().time + msgTimeout)
|
||||||
|
drawLeader()
|
||||||
|
Thread.sleep(250)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
printPrompt(true)
|
|
||||||
} else {
|
} else {
|
||||||
printPrompt(true)
|
drawLeader()
|
||||||
Thread.sleep(200)
|
Thread.sleep(250)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void printPrompt(boolean redraw = false) {
|
private void scanMediaLibrary() {
|
||||||
StringBuilder prompt = new StringBuilder()
|
status.text = "Scanning media library..."
|
||||||
|
library.rescanLibrary()
|
||||||
|
status.text = "Scanned ? files."
|
||||||
|
dismissMsgDate = new Date(new Date().time + msgTimeout) }
|
||||||
|
|
||||||
if (redraw) prompt.append(new ANSI()
|
private void drawLeader(afterOutput = false) {
|
||||||
.saveCursor()
|
|
||||||
.cursorPrevLine(lastPromptLineCount))
|
String leader = beforeLeader + getLeader() +
|
||||||
|
(afterOutput ?
|
||||||
|
(promptStyle + "> " + normalStyle) :
|
||||||
|
afterLeader)
|
||||||
|
|
||||||
|
outStream.print(leader)
|
||||||
|
outStream.flush() }
|
||||||
|
|
||||||
|
private String getLeader() {
|
||||||
|
StringBuilder leader = new StringBuilder()
|
||||||
.append(clearLine)
|
.append(clearLine)
|
||||||
|
.append(titleStyle)
|
||||||
prompt.append(titleStyle)
|
|
||||||
.append("WDIWTLT - v")
|
.append("WDIWTLT - v")
|
||||||
.append(VERSION)
|
.append(VERSION)
|
||||||
.append("-- ")
|
|
||||||
.append(normalStyle)
|
.append(normalStyle)
|
||||||
.append("TODO\n")
|
.append(" -- ")
|
||||||
|
.append(statusStyle)
|
||||||
if (errorMsg) prompt
|
.append(currentlyPlaying)
|
||||||
|
.append("\n")
|
||||||
.append(clearLine)
|
.append(clearLine)
|
||||||
.append(errorStyle)
|
.append(normalStyle)
|
||||||
.append(errorMsg)
|
.append(status)
|
||||||
.append("\n")
|
.append("\n")
|
||||||
|
|
||||||
if (redraw) prompt.append(new ANSI().restoreCursor())
|
return leader.toString() }
|
||||||
else prompt.append(promptStyle)
|
|
||||||
.append("> ")
|
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(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())
|
if (s.size() == 0) status.text = "No current media selections."
|
||||||
out.flush()
|
else status.text = s.toString()
|
||||||
}
|
|
||||||
|
|
||||||
private void replaceCurrentLine(String newLine) {
|
return status.text}
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
||||||
|
}
|
19
cli/src/main/resources/logback.groovy
Normal file
19
cli/src/main/resources/logback.groovy
Normal 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"])
|
@ -17,7 +17,7 @@ public class MediaLibrary {
|
|||||||
|
|
||||||
private static Logger logger = LoggerFactory.getLogger(MediaLibrary)
|
private static Logger logger = LoggerFactory.getLogger(MediaLibrary)
|
||||||
|
|
||||||
private ORM orm
|
@Delegate ORM orm
|
||||||
private File libraryRoot
|
private File libraryRoot
|
||||||
|
|
||||||
public MediaLibrary(ORM orm, File rootDir) {
|
public MediaLibrary(ORM orm, File rootDir) {
|
||||||
@ -66,7 +66,7 @@ public class MediaLibrary {
|
|||||||
mf.name = fileTag?.getFirst(TITLE)?.trim() ?: f.name
|
mf.name = fileTag?.getFirst(TITLE)?.trim() ?: f.name
|
||||||
mf.filePath = relPath
|
mf.filePath = relPath
|
||||||
mf.comment = fileTag?.getFirst(COMMENT)?.trim()
|
mf.comment = fileTag?.getFirst(COMMENT)?.trim()
|
||||||
mf.trackNumber = (fileTag?.getFirst(TRACK) ?: null) as Integer
|
mf.trackNumber = safeToInteger(fileTag?.getFirst(TRACK))
|
||||||
|
|
||||||
def folderParts = mf.filePath.split("[\\\\/]")[1..<-1] as LinkedList
|
def folderParts = mf.filePath.split("[\\\\/]")[1..<-1] as LinkedList
|
||||||
|
|
||||||
@ -91,6 +91,20 @@ public class MediaLibrary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public def getByIdOrName(String input, Class modelClass) {
|
||||||
|
def match
|
||||||
|
if (safeToInteger(input)) match = [orm.getById(safeToInteger(input), modelClass)]
|
||||||
|
else {
|
||||||
|
match = orm.getByName(input, modelClass)
|
||||||
|
if (!match) match = orm.getLike(["name"], [input], modelClass) }
|
||||||
|
return match }
|
||||||
|
|
||||||
|
public List<Artist> getArtistsByName(String name) {
|
||||||
|
return orm.getArtistByName(name) ?: orm.getArtistsLikeName(name) }
|
||||||
|
|
||||||
|
public List<Album> getAlbumsByName(String name) {
|
||||||
|
return orm.getAlbumByName(name) ?: orm.getAlbumsLikeName(name) }
|
||||||
|
|
||||||
private void associateWithArtistAndAlbum(MediaFile mf, String artistName,
|
private void associateWithArtistAndAlbum(MediaFile mf, String artistName,
|
||||||
String albumName, JATag fileTag) {
|
String albumName, JATag fileTag) {
|
||||||
Artist artist = null
|
Artist artist = null
|
||||||
@ -108,9 +122,13 @@ public class MediaLibrary {
|
|||||||
album = orm.getAlbumByName(albumName)
|
album = orm.getAlbumByName(albumName)
|
||||||
if (!album) {
|
if (!album) {
|
||||||
newAlbumOrArtist = true
|
newAlbumOrArtist = true
|
||||||
album = new Album(name: albumName,
|
try {
|
||||||
year: (fileTag?.getFirst(YEAR) ?: null) as Integer,
|
album = new Album(name: albumName,
|
||||||
trackTotal: (fileTag?.getFirst(TRACK_TOTAL) ?: null) as Integer)
|
year: safeToInteger(fileTag?.getFirst(YEAR)),
|
||||||
|
trackTotal: safeToInteger(fileTag?.getFirst(TRACK_TOTAL))) }
|
||||||
|
catch (UnsupportedOperationException use) {
|
||||||
|
album = new Album(name: albumName,
|
||||||
|
year: safeToInteger(fileTag?.getFirst(YEAR))) }
|
||||||
orm.create(album) } }
|
orm.create(album) } }
|
||||||
|
|
||||||
if (artist && album && newAlbumOrArtist)
|
if (artist && album && newAlbumOrArtist)
|
||||||
@ -144,4 +162,8 @@ public class MediaLibrary {
|
|||||||
return (['.'] + childPath[b..<childPath.length]).join('/') }
|
return (['.'] + childPath[b..<childPath.length]).join('/') }
|
||||||
|
|
||||||
|
|
||||||
|
public static Integer safeToInteger(def val) {
|
||||||
|
if (val == null) return null
|
||||||
|
try { return val as Integer }
|
||||||
|
catch (NumberFormatException nfe) { return null } }
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,12 @@ public class ORM {
|
|||||||
public void shutdown() { dataSource.shutdown() }
|
public void shutdown() { dataSource.shutdown() }
|
||||||
|
|
||||||
/// ### Common
|
/// ### Common
|
||||||
|
public def getAll(Class modelClass) {
|
||||||
|
def query = "SELECT * FROM " +
|
||||||
|
pluralize(nameFromModel(modelClass.simpleName))
|
||||||
|
logger.debug("Selecting models.\n\tSQL: {}", query)
|
||||||
|
return sql.rows(query).collect { recordToModel(it, modelClass) } }
|
||||||
|
|
||||||
public def getById(int id, Class modelClass) {
|
public def getById(int id, Class modelClass) {
|
||||||
def query = new StringBuilder()
|
def query = new StringBuilder()
|
||||||
.append("SELECT * FROM ")
|
.append("SELECT * FROM ")
|
||||||
@ -50,7 +56,7 @@ public class ORM {
|
|||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
logger.debug("Selecting model.\n\tSQL: {}\n\tPARAMS: {}", query, name)
|
logger.debug("Selecting model.\n\tSQL: {}\n\tPARAMS: {}", query, name)
|
||||||
return recordToModel(sql.firstRow(query, [name]), modelClass) }
|
return sql.rows(query, [name]).collect { recordToModel(it, modelClass) } }
|
||||||
|
|
||||||
public def getBy(List<String> columns, List<Object> values,
|
public def getBy(List<String> columns, List<Object> values,
|
||||||
Class modelClass) {
|
Class modelClass) {
|
||||||
@ -65,6 +71,20 @@ public class ORM {
|
|||||||
return sql.rows(query, values)
|
return sql.rows(query, values)
|
||||||
.collect { recordToModel(it, modelClass) } }
|
.collect { recordToModel(it, modelClass) } }
|
||||||
|
|
||||||
|
public def getLike(List<String> columns, List<Object> values,
|
||||||
|
Class modelClass) {
|
||||||
|
values = values.collect { "%$it%".toString() }
|
||||||
|
String query = new StringBuilder()
|
||||||
|
.append("SELECT * FROM ")
|
||||||
|
.append(pluralize(nameFromModel(modelClass.simpleName)))
|
||||||
|
.append(" WHERE ")
|
||||||
|
.append(columns.collect { """"$it" LIKE ?"""}.join(' AND '))
|
||||||
|
.toString()
|
||||||
|
|
||||||
|
logger.debug("Selecting models.\n\tSQL: {}\n\tPARAMS: {}", query, values)
|
||||||
|
return sql.rows(query, values)
|
||||||
|
.collect { recordToModel(it, modelClass) } }
|
||||||
|
|
||||||
public def save(def model) {
|
public def save(def model) {
|
||||||
if (model.id > 0) return update(model)
|
if (model.id > 0) return update(model)
|
||||||
else return create(model) }
|
else return create(model) }
|
||||||
@ -130,13 +150,13 @@ public class ORM {
|
|||||||
public def associate(Class modelClass1, Class modelClass2 , Integer firstId, Integer secondId) {
|
public def associate(Class modelClass1, Class modelClass2 , Integer firstId, Integer secondId) {
|
||||||
String linkTable = pluralize(nameFromModel(modelClass1.simpleName)) +
|
String linkTable = pluralize(nameFromModel(modelClass1.simpleName)) +
|
||||||
"_" + pluralize(nameFromModel(modelClass2.simpleName))
|
"_" + pluralize(nameFromModel(modelClass2.simpleName))
|
||||||
String col1 = nameFromModel(modelClass1) + "_id"
|
String col1 = nameFromModel(modelClass1.simpleName) + "_id"
|
||||||
String col2 = nameFromModel(modelClass2) + "_id"
|
String col2 = nameFromModel(modelClass2.simpleName) + "_id"
|
||||||
|
|
||||||
withTransaction {
|
withTransaction {
|
||||||
def query = """\
|
def query = """\
|
||||||
SELECT * FROM $linkTable
|
SELECT * FROM $linkTable
|
||||||
WHERE "${col1}"_id = ? AND "${col2}" = ?"""
|
WHERE "${col1}" = ? AND "${col2}" = ?"""
|
||||||
def params = [firstId, secondId]
|
def params = [firstId, secondId]
|
||||||
|
|
||||||
// Look first for an existing association before creating one.
|
// Look first for an existing association before creating one.
|
||||||
@ -154,7 +174,10 @@ public class ORM {
|
|||||||
|
|
||||||
/// ### Album-specific methods
|
/// ### Album-specific methods
|
||||||
public Album getAlbumById(int id) { return getById(id, Album) }
|
public Album getAlbumById(int id) { return getById(id, Album) }
|
||||||
public Album getAlbumByName(String name) { return getByName(name, Album) }
|
|
||||||
|
public List<Album> getAlbumByName(String name) {
|
||||||
|
return getByName(name, Album) }
|
||||||
|
|
||||||
public Album getAlbumByNameAndArtistId(String name, int artistId) {
|
public Album getAlbumByNameAndArtistId(String name, int artistId) {
|
||||||
def query = """\
|
def query = """\
|
||||||
SELECT al.*
|
SELECT al.*
|
||||||
@ -170,7 +193,7 @@ public class ORM {
|
|||||||
.collect { recordToModel(it, Album) }
|
.collect { recordToModel(it, Album) }
|
||||||
return albums ? albums[0] : null }
|
return albums ? albums[0] : null }
|
||||||
|
|
||||||
public Album getAlbumsByArtistId(int artistId) {
|
public List<Album> getAlbumsByArtistId(int artistId) {
|
||||||
def query = """\
|
def query = """\
|
||||||
SELECT al.*
|
SELECT al.*
|
||||||
FROM albums al JOIN
|
FROM albums al JOIN
|
||||||
@ -182,14 +205,21 @@ public class ORM {
|
|||||||
return sql.rows(query, [artistId])
|
return sql.rows(query, [artistId])
|
||||||
.collect { recordToModel(it, Album) } }
|
.collect { recordToModel(it, Album) } }
|
||||||
|
|
||||||
|
public List<Album> getAlbums() { return getAll(Album) }
|
||||||
|
public List<Album> getAlbumsLikeName(String name) {
|
||||||
|
return getLike(["name"], [name], Album) }
|
||||||
|
|
||||||
public List<Album> removeEmptyAlbums() {
|
public List<Album> removeEmptyAlbums() {
|
||||||
throw new UnsupportedOperationException("Not yet implemented.");
|
throw new UnsupportedOperationException("Not yet implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ### Artist-specific methods
|
/// ### Artist-specific methods
|
||||||
public Artist getArtistById(int id) { return getById(id, Artist) }
|
public Artist getArtistById(int id) { return getById(id, Artist) }
|
||||||
public Artist getArtistByName(String name) { return getByName(name, Artist) }
|
public List<Artist> getArtistByName(String name) {
|
||||||
public Artist getArtistsByAlbum(int albumId) {
|
return getByName(name, Artist) }
|
||||||
|
|
||||||
|
public List<Artist> getArtists() { return getAll(Artist) }
|
||||||
|
public List<Artist> getArtistsByAlbumId(int albumId) {
|
||||||
var query = """\
|
var query = """\
|
||||||
SELECT ar.*
|
SELECT ar.*
|
||||||
FROM artists ar JOIN
|
FROM artists ar JOIN
|
||||||
@ -200,6 +230,9 @@ public class ORM {
|
|||||||
return sql.rows(query, [albumId])
|
return sql.rows(query, [albumId])
|
||||||
.collect { recordToModel(it, Artist) } }
|
.collect { recordToModel(it, Artist) } }
|
||||||
|
|
||||||
|
public List<Artist> getArtistsLikeName(String name) {
|
||||||
|
return getLike(["name"], [name], Artist) }
|
||||||
|
|
||||||
public List<Artist> removeEmptyArtists() {
|
public List<Artist> removeEmptyArtists() {
|
||||||
throw new UnsupportedOperationException("Not yet implemented.");
|
throw new UnsupportedOperationException("Not yet implemented.");
|
||||||
}
|
}
|
||||||
@ -209,14 +242,25 @@ public class ORM {
|
|||||||
|
|
||||||
/// ### Bookmark-specific methods
|
/// ### Bookmark-specific methods
|
||||||
public Bookmark getBookmarkById(int id) { return getById(id, Bookmark) }
|
public Bookmark getBookmarkById(int id) { return getById(id, Bookmark) }
|
||||||
public Bookmark getBookmarkByName(String name) { return getByName(name, Bookmark) }
|
|
||||||
|
public List<Bookmark> getBookmarkByName(String name) {
|
||||||
|
return getByName(name, Bookmark) }
|
||||||
|
|
||||||
|
public List<Bookmark> getBookmarks() { return getAll(Bookmark) }
|
||||||
|
|
||||||
/// ### Image-specific methods
|
/// ### Image-specific methods
|
||||||
public Image getImageById(int id) { return getById(id, Image) }
|
public Image getImageById(int id) { return getById(id, Image) }
|
||||||
|
|
||||||
/// ### MediaFile-specific methods
|
/// ### MediaFile-specific methods
|
||||||
public MediaFile getMediaFileById(int id) { return getById(id, MediaFile) }
|
public MediaFile getMediaFileById(int id) { return getById(id, MediaFile) }
|
||||||
public MediaFile getMediaFileByName(String name) { return getByName(name, MediaFile) }
|
|
||||||
|
public List<MediaFile> getMediaFileByName(String name) {
|
||||||
|
return getByName(name, MediaFile) }
|
||||||
|
|
||||||
|
public List<MediaFile> getMediaFiles() { return getAll(MediaFile) }
|
||||||
|
|
||||||
|
public List<MediaFile> getMediaFilesLikeName(String name) {
|
||||||
|
return getLike(["name"], [name], MediaFile) }
|
||||||
|
|
||||||
public MediaFile getMediaFileByFilePath(String filePath) {
|
public MediaFile getMediaFileByFilePath(String filePath) {
|
||||||
def files = getBy(["file_path"], [filePath], MediaFile)
|
def files = getBy(["file_path"], [filePath], MediaFile)
|
||||||
@ -245,7 +289,11 @@ public class ORM {
|
|||||||
|
|
||||||
/// ### Playlist-specific methods
|
/// ### Playlist-specific methods
|
||||||
public Playlist getPlaylistById(int id) { return getById(id, Playlist) }
|
public Playlist getPlaylistById(int id) { return getById(id, Playlist) }
|
||||||
public Playlist getPlaylistByName(String name) { return getByName(name, Playlist) }
|
|
||||||
|
public List<Playlist> getPlaylistByName(String name) {
|
||||||
|
return getByName(name, Playlist) }
|
||||||
|
|
||||||
|
public List<Playlist> getPlaylists() { return getAll(Playlist) }
|
||||||
|
|
||||||
public List<Playlist> removeEmptyPlaylists() {
|
public List<Playlist> removeEmptyPlaylists() {
|
||||||
throw new UnsupportedOperationException("Not yet implemented.");
|
throw new UnsupportedOperationException("Not yet implemented.");
|
||||||
@ -253,10 +301,13 @@ public class ORM {
|
|||||||
|
|
||||||
/// ### Tag-specific methods
|
/// ### Tag-specific methods
|
||||||
public Tag getTagById(int id) { return getById(id, Tag) }
|
public Tag getTagById(int id) { return getById(id, Tag) }
|
||||||
public Tag getTagByName(String name) { return getByName(name, Tag) }
|
public Tag getTagByName(String name) { return getByName(name, Tag)[0] }
|
||||||
|
|
||||||
/// ### Utility functions
|
/// ### Utility functions
|
||||||
public void withTransaction(Closure c) { sql.withTransaction(c) }
|
public void withTransaction(Closure c) {
|
||||||
|
try { sql.execute("BEGIN TRANSACTION"); c() }
|
||||||
|
finally { sql.execute("COMMIT") }
|
||||||
|
}
|
||||||
public static String nameToModel(String name) {
|
public static String nameToModel(String name) {
|
||||||
def pts = name.toLowerCase().split('_')
|
def pts = name.toLowerCase().split('_')
|
||||||
return pts.length == 1 ? pts[0] :
|
return pts.length == 1 ? pts[0] :
|
||||||
|
Loading…
Reference in New Issue
Block a user