From 2df519f0f67a0d8512a509209e8b96515d7afe6c Mon Sep 17 00:00:00 2001 From: Adrian Ulrich Date: Sat, 26 Nov 2016 18:57:41 +0100 Subject: [PATCH] add metadata extraction wrapper --- .../android/medialibrary/MediaLibrary.java | 4 +- .../medialibrary/MediaMetadataExtractor.java | 152 ++++++++++++++++++ .../android/medialibrary/MediaScanner.java | 83 +++++----- 3 files changed, 192 insertions(+), 47 deletions(-) create mode 100644 src/ch/blinkenlights/android/medialibrary/MediaMetadataExtractor.java diff --git a/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java b/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java index f9e7829d..340d146b 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaLibrary.java @@ -53,8 +53,8 @@ public class MediaLibrary { sBackend = new MediaLibraryBackend(context); sScanner = new MediaScanner(sBackend); - File dir = new File("/storage"); -// sScanner.startScan(dir); + File dir = new File("/storage/emulated/0"); + sScanner.startScan(dir); } // } } diff --git a/src/ch/blinkenlights/android/medialibrary/MediaMetadataExtractor.java b/src/ch/blinkenlights/android/medialibrary/MediaMetadataExtractor.java new file mode 100644 index 00000000..6ca3b477 --- /dev/null +++ b/src/ch/blinkenlights/android/medialibrary/MediaMetadataExtractor.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2016 Adrian Ulrich + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package ch.blinkenlights.android.medialibrary; + +import ch.blinkenlights.bastp.Bastp; +import android.media.MediaMetadataRetriever; +import java.util.HashMap; +import java.util.ArrayList; + +public class MediaMetadataExtractor extends HashMap> { + // Well known tags + public final static String ALBUM = "ALBUM"; + public final static String ALBUMARTIST = "ALBUM_ARTIST"; + public final static String ARTIST = "ARTIST"; + public final static String BITRATE = "BITRATE"; + public final static String COMPOSER = "COMPOSER"; + public final static String DISC_COUNT = "DISC_COUNT"; + public final static String DISC_NUMBER = "DISC_NUMBER"; + public final static String DURATION = "DURATION"; + public final static String GENRE = "GENRE"; + public final static String TRACK_COUNT = "TRACK_COUNT"; + public final static String TRACK_NUMBER = "TRACK_NUM"; + public final static String TITLE = "TITLE"; + public final static String YEAR = "YEAR"; + + /** + * Constructor for MediaMetadataExtractor + * + * @param path the path to scan + */ + MediaMetadataExtractor(String path) { + extractMetadata(path); + } + + /** + * Returns the first element matching this key, null on if not found + * + * @param key the key to look up + * @return the value of the first entry, null if the key was not found + */ + public String getFirst(String key) { + String result = null; + if (containsKey(key)) + result = get(key).get(0); + return result; + } + + /** + * Attempts to populate this instance with tags found in given path + * + * @param path the path to parse + * @return void, but populates `this' + */ + private void extractMetadata(String path) { + if (isEmpty() == false) + throw new IllegalStateException("Expected to be called on a clean HashMap"); + + HashMap bastpTags = (new Bastp()).getTags(path); + MediaMetadataRetriever mediaTags = new MediaMetadataRetriever(); + try { + mediaTags.setDataSource(path); + } catch (Exception e) { /* we will later just check the contents of mediaTags */ } + + // Check if this is an useable audio file + if (mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) == null || + mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) != null || + mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) == null) { + mediaTags.release(); + return; + } + + // Bastp can not read the duration and bitrates, so we always get it from the system + ArrayList duration = new ArrayList(1); + duration.add(mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)); + this.put(DURATION, duration); + + ArrayList bitrate = new ArrayList(1); + bitrate.add(mediaTags.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)); + this.put(BITRATE, bitrate); + + + // ...but we are using bastp for FLAC, OGG and OPUS as it handles them well + // Everything else goes to the framework (such as pcm, m4a and mp3) + String bastpType = (bastpTags.containsKey("type") ? (String)bastpTags.get("type") : ""); + switch (bastpType) { + case "FLAC": + case "OGG": + case "OPUS": + populateSelf(bastpTags); + break; + default: + populateSelf(mediaTags); + } + + mediaTags.release(); + } + + /** + * Populates `this' with tags read from bastp + * + * @param bastp A hashmap as returned by bastp + */ + private void populateSelf(HashMap bastp) { + // mapping between vorbiscomment -> constant + String[] map = new String[]{ "TITLE", TITLE, "ARTIST", ARTIST, "ALBUM", ALBUM, "ALBUMARTIST", ALBUMARTIST, "COMPOSER", COMPOSER, "GENRE", GENRE, + "TRACKNUMBER", TRACK_NUMBER, "TRACKTOTAL", TRACK_COUNT, "DISCNUMBER", DISC_NUMBER, "DISCTOTAL", DISC_COUNT, + "YEAR", YEAR }; + for (int i=0; i tags = (ArrayList)bastp.get(map[i]); + put(map[i+1], tags); + } + } + } + + /** + * Populates `this' with tags read from the MediaMetadataRetriever + * + * @param tags a MediaMetadataRetriever object + */ + private void populateSelf(MediaMetadataRetriever tags) { + int[] mediaMap = new int[] { MediaMetadataRetriever.METADATA_KEY_TITLE, MediaMetadataRetriever.METADATA_KEY_ARTIST, MediaMetadataRetriever.METADATA_KEY_ALBUM, + MediaMetadataRetriever.METADATA_KEY_ALBUMARTIST, MediaMetadataRetriever.METADATA_KEY_COMPOSER, MediaMetadataRetriever.METADATA_KEY_GENRE, + MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER, MediaMetadataRetriever.METADATA_KEY_YEAR }; + String[] selfMap = new String[]{ TITLE, ARTIST, ALBUM, ALBUMARTIST, COMPOSER, GENRE, TRACK_NUMBER, YEAR }; + for (int i=0; i md = new ArrayList(1); + md.add(data); + put(selfMap[i], md); + } + } + } + + +} diff --git a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java index 5795311c..4be2d4dd 100644 --- a/src/ch/blinkenlights/android/medialibrary/MediaScanner.java +++ b/src/ch/blinkenlights/android/medialibrary/MediaScanner.java @@ -27,8 +27,8 @@ import android.os.Message; import android.os.Process; import java.io.File; +import java.util.ArrayList; import java.util.HashMap; -import java.util.Vector; public class MediaScanner implements Handler.Callback { /** @@ -105,90 +105,83 @@ public class MediaScanner implements Handler.Callback { String path = file.getAbsolutePath(); long songId = MediaLibrary.hash63(path); - HashMap tags = (new Bastp()).getTags(path); - if (tags.containsKey("type") == false) - return; // no tags found - -Log.v("VanillaMusic", "> Found mime "+((String)tags.get("type"))); - if (mBackend.isSongExisting(songId)) { Log.v("VanillaMusic", "Skipping already known song with id "+songId); return; } - MediaMetadataRetriever data = new MediaMetadataRetriever(); - try { - data.setDataSource(path); - } catch (Exception e) { - Log.w("VanillaMusic", "Failed to extract metadata from " + path); - } + MediaMetadataExtractor tags = new MediaMetadataExtractor(path); + if (tags.isEmpty()) + return; // file does not contain audio data - String duration = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); - if (duration == null) - return; // not a supported media file! + // Get tags which always must be set + String title = tags.getFirst(MediaMetadataExtractor.TITLE); + if (title == null) + title = "Untitled"; - if (data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_AUDIO) == null) - return; // no audio -> do not index + String album = tags.getFirst(MediaMetadataExtractor.ALBUM); + if (album == null) + album = "No Album"; - if (data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_HAS_VIDEO) != null) - return; // has a video stream -> do not index + String artist = tags.getFirst(MediaMetadataExtractor.ARTIST); + if (artist == null) + artist = "No Artist"; - String title = (tags.containsKey("TITLE") ? (String)((Vector)tags.get("TITLE")).get(0) : "Untitled"); - String album = (tags.containsKey("ALBUM") ? (String)((Vector)tags.get("ALBUM")).get(0) : "No Album"); - String artist = (tags.containsKey("ARTIST") ? (String)((Vector)tags.get("ARTIST")).get(0) : "Unknown Artist"); - - String songnum = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_CD_TRACK_NUMBER); - String composer = data.extractMetadata(MediaMetadataRetriever.METADATA_KEY_COMPOSER); long albumId = MediaLibrary.hash63(album); long artistId = MediaLibrary.hash63(artist); - long composerId = MediaLibrary.hash63(composer); ContentValues v = new ContentValues(); - v.put(MediaLibrary.SongColumns._ID, songId); - v.put(MediaLibrary.SongColumns.TITLE, title); - v.put(MediaLibrary.SongColumns.TITLE_SORT, MediaLibrary.keyFor(title)); - v.put(MediaLibrary.SongColumns.ALBUM_ID, albumId); - v.put(MediaLibrary.SongColumns.DURATION, duration); - v.put(MediaLibrary.SongColumns.SONG_NUMBER,songnum); - v.put(MediaLibrary.SongColumns.PATH, path); + v.put(MediaLibrary.SongColumns._ID, songId); + v.put(MediaLibrary.SongColumns.TITLE, title); + v.put(MediaLibrary.SongColumns.TITLE_SORT, MediaLibrary.keyFor(title)); + v.put(MediaLibrary.SongColumns.ALBUM_ID, albumId); + v.put(MediaLibrary.SongColumns.DURATION, tags.getFirst(MediaMetadataExtractor.DURATION)); + v.put(MediaLibrary.SongColumns.SONG_NUMBER, tags.getFirst(MediaMetadataExtractor.TRACK_NUMBER)); + v.put(MediaLibrary.SongColumns.PATH, path); mBackend.insert(MediaLibrary.TABLE_SONGS, null, v); v.clear(); - v.put(MediaLibrary.AlbumColumns._ID, albumId); - v.put(MediaLibrary.AlbumColumns.ALBUM, album); - v.put(MediaLibrary.AlbumColumns.ALBUM_SORT, MediaLibrary.keyFor(album)); + v.put(MediaLibrary.AlbumColumns._ID, albumId); + v.put(MediaLibrary.AlbumColumns.ALBUM, album); + v.put(MediaLibrary.AlbumColumns.ALBUM_SORT, MediaLibrary.keyFor(album)); v.put(MediaLibrary.AlbumColumns.PRIMARY_ARTIST_ID, artistId); + v.put(MediaLibrary.AlbumColumns.YEAR, tags.getFirst(MediaMetadataExtractor.YEAR)); + v.put(MediaLibrary.AlbumColumns.DISC_NUMBER, tags.getFirst(MediaMetadataExtractor.DISC_NUMBER)); mBackend.insert(MediaLibrary.TABLE_ALBUMS, null, v); v.clear(); - v.put(MediaLibrary.ContributorColumns._ID, artistId); + v.put(MediaLibrary.ContributorColumns._ID, artistId); v.put(MediaLibrary.ContributorColumns._CONTRIBUTOR, artist); v.put(MediaLibrary.ContributorColumns._CONTRIBUTOR_SORT, MediaLibrary.keyFor(artist)); mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS, null, v); v.clear(); v.put(MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID, artistId); - v.put(MediaLibrary.ContributorSongColumns.SONG_ID, songId); - v.put(MediaLibrary.ContributorSongColumns.ROLE, 0); + v.put(MediaLibrary.ContributorSongColumns.SONG_ID, songId); + v.put(MediaLibrary.ContributorSongColumns.ROLE, 0); mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS_SONGS, null, v); + // Composers are optional: only add if we found it + String composer = tags.getFirst(MediaMetadataExtractor.COMPOSER); if (composer != null) { + long composerId = MediaLibrary.hash63(composer); v.clear(); - v.put(MediaLibrary.ContributorColumns._ID, composerId); + v.put(MediaLibrary.ContributorColumns._ID, composerId); v.put(MediaLibrary.ContributorColumns._CONTRIBUTOR, composer); v.put(MediaLibrary.ContributorColumns._CONTRIBUTOR_SORT, MediaLibrary.keyFor(composer)); mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS, null, v); v.clear(); v.put(MediaLibrary.ContributorSongColumns._CONTRIBUTOR_ID, composerId); - v.put(MediaLibrary.ContributorSongColumns.SONG_ID, songId); - v.put(MediaLibrary.ContributorSongColumns.ROLE, 1); + v.put(MediaLibrary.ContributorSongColumns.SONG_ID, songId); + v.put(MediaLibrary.ContributorSongColumns.ROLE, 1); mBackend.insert(MediaLibrary.TABLE_CONTRIBUTORS_SONGS, null, v); } - if (tags.containsKey("GENRE")) { - Vector genres = (Vector)tags.get("GENRE"); + // A song might be in multiple genres + if (tags.containsKey(MediaMetadataExtractor.GENRE)) { + ArrayList genres = tags.get(MediaMetadataExtractor.GENRE); for (String genre : genres) { long genreId = MediaLibrary.hash63(genre); v.clear();