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 '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'

View File

@ -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
}
} }

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"])

View 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 } }
} }

View File

@ -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] :