diff --git a/service/src/main/groovy/com/jdbernard/wdiwtlt/MediaLibrary.groovy b/service/src/main/groovy/com/jdbernard/wdiwtlt/MediaLibrary.groovy index 2dc40bd..0d46a8e 100644 --- a/service/src/main/groovy/com/jdbernard/wdiwtlt/MediaLibrary.groovy +++ b/service/src/main/groovy/com/jdbernard/wdiwtlt/MediaLibrary.groovy @@ -9,6 +9,7 @@ import org.jaudiotagger.tag.Tag as JATag import org.jaudiotagger.tag.FieldKey import org.slf4j.Logger import org.slf4j.LoggerFactory +import org.apache.commons.codec.digest.DigestUtils import static org.jaudiotagger.tag.FieldKey.* @@ -23,7 +24,7 @@ public class MediaLibrary { logger.debug("Creating a MediaLibrary rooted at: {}", rootDir.canonicalPath) - this.orm = org + this.orm = orm this.libraryRoot = rootDir } public void clean() { @@ -32,33 +33,46 @@ public class MediaLibrary { orm.removeEmptyPlaylists() } + public void rescanLibrary() { + libraryRoot.eachFileRecurse { addFile(it) } + } + public MediaFile addFile(File f) { if (!f.exists() || !f.isFile()) { - logger.debug("Ignoring non-existant file: {}", f.canonicalPath) + logger.info("Ignoring non-existant file: {}", f.canonicalPath) return null } def relPath = getRelativePath(libraryRoot, f) MediaFile mf = orm.getMediaFileByFilePath(relPath) if (mf) { - logger.debug("Ignoring a media file I already know about: {}", - relPath) + logger.info( + "Ignoring a media file I already know about: {}", relPath) return mf } // Read in the media's tags mf = new MediaFile() - def af = AudioFileIO.read(f) + def af + + try { af = AudioFileIO.read(f) } + catch (Exception e) { + logger.info("Ignoring a file because I can't" + + "read the media tag info:\n\t{}", + e.localizedMessage) + return null } + def fileTag = af.tag - mf.name = fileTag.getFirst(TITLE).trim() ?: f.name + mf.name = fileTag?.getFirst(TITLE)?.trim() ?: f.name mf.filePath = relPath - mf.comment = fileTag.getFirst(COMMENT).trim() + mf.comment = fileTag?.getFirst(COMMENT)?.trim() + mf.trackNumber = (fileTag?.getFirst(TRACK) ?: null) as Integer - def folderParts = mf.filePath.split("[\\\\/]")[1..<-1] + def folderParts = mf.filePath.split("[\\\\/]")[1..<-1] as LinkedList // Find artist and album names (if any) - def artistName = fileTag.getFirst(ARTIST).trim() - def albumName = fileTag.getFirst(ALBUM).trim() + def artistName = fileTag?.getFirst(ARTIST)?.trim() + def albumName = fileTag?.getFirst(ALBUM)?.trim() if (!artistName) { mf.metaInfoSource = MediaFile.FILE_LOCATION @@ -70,34 +84,34 @@ public class MediaLibrary { // Hash the file mf.fileHash = f.withInputStream { DigestUtils.md5Hex(it) } - orm.create(mf) - associateWithArtistAndAlbum(mf, artistName, albumName) + orm.withTransaction { + orm.create(mf) + associateWithArtistAndAlbum(mf, artistName, albumName, fileTag) + } } private void associateWithArtistAndAlbum(MediaFile mf, String artistName, - String albumName) { + String albumName, JATag fileTag) { Artist artist = null Album album = null boolean newAlbumOrArtist = false - if (albumName) { - album = orm.findAlbumByName(albumName) - if (!album) { - newAlbumOrArtist = true - album = new Album(name: albumName, - year: fileTag.getFirst(YEAR) ?: null) - orm.create(album) } } - if (artistName) { - artist = orm.findArtistByName(artistName) + artist = orm.getArtistByName(artistName) if (!artist) { newAlbumOrArtist = true artist = new Artist(name: artistName) orm.create(artist) } } - // TODO: need to rethink for case where another album does alrady exist - // by this name, but is already associated with a different artist. + if (albumName) { + album = orm.getAlbumByName(albumName) + if (!album) { + newAlbumOrArtist = true + album = new Album(name: albumName, + year: (fileTag?.getFirst(YEAR) ?: null) as Integer, + trackTotal: (fileTag?.getFirst(TRACK_TOTAL) ?: null) as Integer) + orm.create(album) } } if (artist && album && newAlbumOrArtist) orm.addAlbumArtist(album.id, artist.id) diff --git a/service/src/main/groovy/com/jdbernard/wdiwtlt/db/ORM.groovy b/service/src/main/groovy/com/jdbernard/wdiwtlt/db/ORM.groovy index dcd3976..2a93a1a 100644 --- a/service/src/main/groovy/com/jdbernard/wdiwtlt/db/ORM.groovy +++ b/service/src/main/groovy/com/jdbernard/wdiwtlt/db/ORM.groovy @@ -1,5 +1,6 @@ package com.jdbernard.wdiwtlt.db +import java.lang.reflect.Modifier import java.sql.Connection import java.sql.PreparedStatement import java.sql.ResultSet @@ -11,6 +12,9 @@ import groovy.sql.Sql import groovy.transform.CompileStatic import groovy.transform.TailRecursive +import org.slf4j.Logger +import org.slf4j.LoggerFactory + import com.jdbernard.wdiwtlt.db.models.* public class ORM { @@ -19,6 +23,7 @@ public class ORM { private Sql sql private static UPPERCASE_PATTERN = Pattern.compile(/(.)(\p{javaUpperCase})/) + private static Logger logger = LoggerFactory.getLogger(ORM) public ORM(DataSource dataSource) { this.dataSource = dataSource @@ -34,7 +39,8 @@ public class ORM { .append(" WHERE id = ?") .toString() - return recordToModel(sql.firstRow(query, id), modelClass) } + logger.debug("Selecting model.\n\tSQL: {}\n\tPARAMS: {}", query, id) + return recordToModel(sql.firstRow(query, [id]), modelClass) } public def getByName(String name, Class modelClass) { def query = new StringBuilder() @@ -43,7 +49,8 @@ public class ORM { .append(" WHERE name = ?") .toString() - return recordToModel(sql.firstRow(query, name), modelClass) } + logger.debug("Selecting model.\n\tSQL: {}\n\tPARAMS: {}", query, name) + return recordToModel(sql.firstRow(query, [name]), modelClass) } public def getBy(List columns, List values, Class modelClass) { @@ -54,6 +61,7 @@ public class ORM { .append(columns.collect { it + " = ?" }.join(' AND ')) .toString() + logger.debug("Selecting models.\n\tSQL: {}\n\tPARAMS: {}", query, values) return sql.rows(query, values) .collect { recordToModel(it, modelClass) } } @@ -65,7 +73,7 @@ public class ORM { def setClauses = [] def params = [] - model.class.fields + getInstanceFields(model.class) .findAll { it.name != 'id' } .each { field -> setClauses << '"' + nameFromModel(field.name) + '"= ?' @@ -80,13 +88,14 @@ public class ORM { .append(model.id) .toString() + logger.debug("Updating model.\n\tSQL: {}\n\tPARAMS: {}", query, params) return sql.executeUpdate(query, params) } public def create(def model) { def columns = [] def params = [] - model.class.fields + getInstanceFields(model.class) .findAll { it.name != 'id' } .each { field -> //if (field.class.getAnnotation(Model)) // check to see if we @@ -103,17 +112,27 @@ public class ORM { .append((1..columns.size()).collect { '?' }.join(', ')) .append(")").toString() + logger.debug("Creating model.\n\tSQL: {}\n\tPARAMS: {}", query, params) model.id = sql.executeInsert(query, params)[0][0] return 1 } public def delete(def model) { - sql.execute("DELETE FROM ${model.class.simpleName} WHERE id = ?", [model.id]) + def query = new StringBuilder() + .append("DELETE FROM ") + .append(pluralize(nameFromModel(model.class.simpleName))) + .append("WHERE id = ?") + .toString() + + logger.debug("Deleting model.\n\tSQL: {}\n\tPARAMS: {}", query, model.id) + sql.execute(query, [model.id]) return sql.updateCount } - public def associate(String linkTable, int firstId, int secondId) { - return sql.executeInsert( - "INSERT INTO $linkTable VALUES (?, ?) ON CONFLICT DO NOTHING", - [firstId, secondId]) } + public def associate(String linkTable, Integer firstId, Integer secondId) { + def query = "INSERT INTO $linkTable VALUES (?, ?) ON CONFLICT DO NOTHING" + def params = [firstId, secondId] + logger.debug("Creating association.\n\tSQL: {}\n\tPARAMS: {}", + query, params) + return sql.execute(query, params) } public def associate(def m1, def m2) { return associate(pluralize(nameFromModel(m1.class.simpleName)) + @@ -124,24 +143,31 @@ public class ORM { public Album getAlbumById(int id) { return getById(id, Album) } public Album getAlbumByName(String name) { return getByName(name, Album) } public Album getAlbumByNameAndArtistId(String name, int artistId) { - def albums = sql.rows("""\ + def query = """\ SELECT al.* FROM albums al JOIN artists_albums aa ON al.id = aa.album_id AND aa.artist_id = ? - WHERE al.name = ?""", - [artistId, name]).collect { recordToModel(it, Album) } + WHERE al.name = ?""" + def params = [artistId, name] + logger.debug("Selecting albums.\n\tSQL: {}\n\tPARAMS: {}", + query, params) + def albums = sql.rows(query, params) + .collect { recordToModel(it, Album) } return albums ? albums[0] : null } public Album getAlbumsByArtistId(int artistId) { - return sql.rows("""\ + def query = """\ SELECT al.* FROM albums al JOIN artists_albums aa ON al.id = aa.album_id AND - aa.artist_id = ?""", - [artistId]).collect { recordToModel(it, Album) } } + aa.artist_id = ?""" + + logger.debug("Selecting albums.\n\tSQL: {}\n\tPARAMS: {}", query, artistId) + return sql.rows(query, [artistId]) + .collect { recordToModel(it, Album) } } public List removeEmptyAlbums() { throw new UnsupportedOperationException("Not yet implemented."); @@ -151,13 +177,15 @@ public class ORM { public Artist getArtistById(int id) { return getById(id, Artist) } public Artist getArtistByName(String name) { return getByName(name, Artist) } public Artist getArtistsByAlbum(int albumId) { - return sql.rows("""\ + var query = """\ SELECT ar.* FROM artists ar JOIN artists_albums aa ON ar.id = aa.artist_id AND - aa.album_id = ?""", - [albumId]).collect { recordToModel(it, Artist) } } + aa.album_id = ?""" + logger.debug("Selecting artists.\n\tSQL: {}\n\tPARAMS: {}", query, artistId) + return sql.rows(query, [albumId]) + .collect { recordToModel(it, Artist) } } public List removeEmptyArtists() { throw new UnsupportedOperationException("Not yet implemented."); @@ -177,12 +205,31 @@ public class ORM { public MediaFile getMediaFileById(int id) { return getById(id, MediaFile) } public MediaFile getMediaFileByName(String name) { return getByName(name, MediaFile) } + public MediaFile getMediaFileByFilePath(String filePath) { + def files = getBy(["file_path"], [filePath], MediaFile) + return files ? files[0] : null } + public def associateMediaFileWithAlbum(int mediaFileId, int albumId) { return associate("albums_media_files", albumId, mediaFileId) } public def associateMediaFileWithArtist(int mediaFileId, int artistId) { return associate("artists_media_files", artistId, mediaFileId) } + public def incrementPlayCount(int mediaFileId) { + def query = "UPDATE media_files SET play_count = play_count + 1 WHERE ID = ?" + def params = [mediaFileId] + + logger.debug("Updating media file.\n\tSQL: {}\n\tPARAMS: {}", query, params) + sql.executeUpdate(query, params) + + query = "SELECT play_count FROM media_files WHERE id = ?" + logger.debug("Selecting media file play count.\n\tSQL: {}\n\tPARAMS: {}", query, params) + return sql.firstRow(query, params)[0] } + + public MediaFile incrementPlayCount(MediaFile mf) { + mf.playCount = incrementPlayCount(mf.id) + return mf } + /// ### Playlist-specific methods public Playlist getPlaylistById(int id) { return getById(id, Playlist) } public Playlist getPlaylistByName(String name) { return getByName(name, Playlist) } @@ -196,6 +243,7 @@ public class ORM { public Tag getTagByName(String name) { return getByName(name, Tag) } /// ### Utility functions + public void withTransaction(Closure c) { sql.withTransaction(c) } public static String nameToModel(String name) { def pts = name.toLowerCase().split('_') return pts.length == 1 ? pts[0] : @@ -208,7 +256,7 @@ public class ORM { public static String pluralize(String name) { return name + "s" } static def updateModel(def record, def model) { - model.class.fields.each { field -> + getInstanceFields(model.class).each { field -> field.set(model, record[nameFromModel(field.name)]) } return model } @@ -217,7 +265,7 @@ public class ORM { def model = clazz.newInstance() - model.class.fields.each { field -> + getInstanceFields(model.class).each { field -> field.set(model, record[nameFromModel(field.name)]) } return model } @@ -227,8 +275,11 @@ public class ORM { def record = [:] - model.class.fields.each { field -> + getInstanceFields(model.class).each { field -> record[nameFromModel(field.name)] = field.get(model) } return record } + + static def getInstanceFields(Class clazz) { + return clazz.fields.findAll { !Modifier.isStatic(it.modifiers) } } }