Rescanning now finds moved files by their hash.

More precisely, the MediaLibrary matches up files based on their file hash. If
it finds a files that matches an already known entry it doesn't create a new
entry, only updates the file path to the found file in the existing entry.

This is useful if files get re-organized or if an existing database is loaded
at an entirely new library root that contains mostly the same files (relocating
the library to another computer for example). This method preserves information
added to the database that was not originally present in the ID3 tags in the
media files (custom tags, name corrections, etc).
This commit is contained in:
Jonathan Bernard 2016-03-16 05:49:29 -05:00
parent 07fa7559ac
commit d2ed22d229
2 changed files with 23 additions and 9 deletions

View File

@ -70,17 +70,31 @@ public class MediaLibrary {
return null } return null }
def relPath = getRelativePath(libraryRoot, f) def relPath = getRelativePath(libraryRoot, f)
MediaFile mf = dbapi.getMediaFileByFilePath(relPath) MediaFile found = dbapi.getMediaFileByFilePath(relPath)
if (mf) { if (found) {
logger.info( logger.info(
"Ignoring a media file I already know about: {}", relPath) "Ignoring a media file I already know about: {}", relPath)
return mf } return found }
MediaFile mf = new MediaFile()
mf.filePath = relPath
// Hash the file
mf.fileHash = f.withInputStream { DigestUtils.md5Hex(it) }
// Look for an entry for that hash
found = dbapi.getMediaFileByFileHash(mf.fileHash)
if (found) {
logger.info('Found a media file by hash in a new location. ' +
"I'm updating my relative path to this file:\n\t{}\n\t\t--> {}.",
found.filePath, mf.filePath)
found.filePath = mf.filePath
return dbapi.update(found) }
// Read in the media's tags // Read in the media's tags
mf = new MediaFile()
def af def af
try { af = AudioFileIO.read(f) } try { af = AudioFileIO.read(f) }
catch (Exception e) { catch (Exception e) {
logger.info("Ignoring a file because I can't " + logger.info("Ignoring a file because I can't " +
@ -91,7 +105,6 @@ public class MediaLibrary {
def fileTag = af.tag 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?.getAll(COMMENT)?.collect { it.trim() }?.join('\n\n') mf.comment = fileTag?.getAll(COMMENT)?.collect { it.trim() }?.join('\n\n')
mf.discNumber = safeToInteger(fileTag?.getFirst(DISC_NO)) ?: 1 mf.discNumber = safeToInteger(fileTag?.getFirst(DISC_NO)) ?: 1
mf.trackNumber = safeToInteger(fileTag?.getFirst(TRACK)) mf.trackNumber = safeToInteger(fileTag?.getFirst(TRACK))
@ -110,9 +123,6 @@ public class MediaLibrary {
mf.metaInfoSource = MediaFile.FILE_LOCATION mf.metaInfoSource = MediaFile.FILE_LOCATION
albumNames = [folderParts.peekLast()] } albumNames = [folderParts.peekLast()] }
// Hash the file
mf.fileHash = f.withInputStream { DigestUtils.md5Hex(it) }
dbapi.withTransaction { dbapi.withTransaction {
dbapi.create(mf) dbapi.create(mf)
associateWithArtistsAndAlbums(mf, artistNames, albumNames, associateWithArtistsAndAlbums(mf, artistNames, albumNames,

View File

@ -411,6 +411,10 @@ public class DbApi {
def files = getBy(MediaFile, ['file_path'], [filePath]) def files = getBy(MediaFile, ['file_path'], [filePath])
return files ? files[0] : null } return files ? files[0] : null }
public MediaFile getMediaFileByFileHash(String fileHash) {
def files = getBy(MediaFile, ['file_hash'], [fileHash])
return files ? files[0] : null }
public List<MediaFile> getMediaFilesWhere(Map params) { public List<MediaFile> getMediaFilesWhere(Map params) {
def query = new StringBuilder() def query = new StringBuilder()
def sqlParams = [] def sqlParams = []