Merge pull request #155 from tkashkin/lyrics

Add synchronized lyrics support (only LRC format at the moment)
This commit is contained in:
Eugene Cheung 2017-07-15 12:00:24 -04:00 committed by GitHub
commit 77290b4ad3
15 changed files with 486 additions and 93 deletions

View file

@ -2,22 +2,21 @@ package com.kabouzeid.gramophone.dialogs;
import android.app.Dialog;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import com.afollestad.materialdialogs.MaterialDialog;
import com.kabouzeid.gramophone.model.lyrics.Lyrics;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public class LyricsDialog extends DialogFragment {
public static LyricsDialog create(@NonNull LyricInfo lyricInfo) {
public static LyricsDialog create(@NonNull Lyrics lyrics) {
LyricsDialog dialog = new LyricsDialog();
Bundle args = new Bundle();
args.putParcelable("LyricInfo", lyricInfo);
args.putString("title", lyrics.song.title);
args.putString("lyrics", lyrics.getText());
dialog.setArguments(args);
return dialog;
}
@ -25,49 +24,10 @@ public class LyricsDialog extends DialogFragment {
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
LyricInfo lyricInfo = getArguments().getParcelable("LyricInfo");
//noinspection ConstantConditions
return new MaterialDialog.Builder(getActivity())
.title(lyricInfo.title)
.content(lyricInfo.lyrics)
.title(getArguments().getString("title"))
.content(getArguments().getString("lyrics"))
.build();
}
public static class LyricInfo implements Parcelable {
public final String title;
public final String lyrics;
public LyricInfo(String title, String lyrics) {
this.title = title;
this.lyrics = lyrics;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeString(this.title);
dest.writeString(this.lyrics);
}
protected LyricInfo(Parcel in) {
this.title = in.readString();
this.lyrics = in.readString();
}
public static final Parcelable.Creator<LyricInfo> CREATOR = new Parcelable.Creator<LyricInfo>() {
@Override
public LyricInfo createFromParcel(Parcel source) {
return new LyricInfo(source);
}
@Override
public LyricInfo[] newArray(int size) {
return new LyricInfo[size];
}
};
}
}

View file

@ -15,6 +15,8 @@ public class MusicProgressViewUpdateHelper extends Handler {
private static final int UPDATE_INTERVAL_PAUSED = 500;
private Callback callback;
private int intervalPlaying;
private int intervalPaused;
public void start() {
queueNextRefresh(1);
@ -26,6 +28,14 @@ public class MusicProgressViewUpdateHelper extends Handler {
public MusicProgressViewUpdateHelper(Callback callback) {
this.callback = callback;
this.intervalPlaying = UPDATE_INTERVAL_PLAYING;
this.intervalPaused = UPDATE_INTERVAL_PAUSED;
}
public MusicProgressViewUpdateHelper(Callback callback, int intervalPlaying, int intervalPaused) {
this.callback = callback;
this.intervalPlaying = intervalPlaying;
this.intervalPaused = intervalPaused;
}
@Override
@ -43,10 +53,10 @@ public class MusicProgressViewUpdateHelper extends Handler {
callback.onUpdateProgressViews(progressMillis, totalMillis);
if (!MusicPlayerRemote.isPlaying()) {
return UPDATE_INTERVAL_PAUSED;
return intervalPaused;
}
final int remainingMillis = UPDATE_INTERVAL_PLAYING - progressMillis % UPDATE_INTERVAL_PLAYING;
final int remainingMillis = intervalPlaying - progressMillis % intervalPlaying;
return Math.max(MIN_INTERVAL, remainingMillis);
}

View file

@ -0,0 +1,55 @@
package com.kabouzeid.gramophone.model.lyrics;
import android.util.SparseArray;
public abstract class AbsSynchronizedLyrics extends Lyrics {
private static final int TIME_OFFSET_MS = 500; // time adjustment to display line before it actually starts
protected final SparseArray<String> lines = new SparseArray<>();
protected int offset = 0;
public String getLine(int time) {
time += offset + AbsSynchronizedLyrics.TIME_OFFSET_MS;
int lastLineTime = lines.keyAt(0);
for (int i = 0; i < lines.size(); i++) {
int lineTime = lines.keyAt(i);
if (time >= lineTime) {
lastLineTime = lineTime;
} else {
break;
}
}
return lines.get(lastLineTime);
}
public boolean isSynchronized() {
return true;
}
public boolean isValid() {
parse(true);
return valid;
}
@Override
public String getText() {
parse(false);
if (valid) {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < lines.size(); i++) {
String line = lines.valueAt(i);
sb.append(line).append("\r\n");
}
return sb.toString().trim().replaceAll("(\r?\n){3,}", "\r\n\r\n");
}
return super.getText();
}
}

View file

@ -0,0 +1,68 @@
package com.kabouzeid.gramophone.model.lyrics;
import com.kabouzeid.gramophone.model.Song;
import java.util.ArrayList;
public class Lyrics {
private static final ArrayList<Class<? extends Lyrics>> FORMATS = new ArrayList<>();
public Song song;
public String data;
protected boolean parsed = false;
protected boolean valid = false;
public Lyrics setData(Song song, String data) {
this.song = song;
this.data = data;
return this;
}
public static Lyrics parse(Song song, String data) {
for (Class<? extends Lyrics> format : Lyrics.FORMATS) {
try {
Lyrics lyrics = format.newInstance().setData(song, data);
if (lyrics.isValid()) return lyrics.parse(false);
} catch (Exception e) {
e.printStackTrace();
}
}
return new Lyrics().setData(song, data).parse(false);
}
public static boolean isSynchronized(String data) {
for (Class<? extends Lyrics> format : Lyrics.FORMATS) {
try {
Lyrics lyrics = format.newInstance().setData(null, data);
if (lyrics.isValid()) return true;
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
public Lyrics parse(boolean check) {
this.valid = true;
this.parsed = true;
return this;
}
public boolean isSynchronized() {
return false;
}
public boolean isValid() {
this.parse(true);
return this.valid;
}
public String getText() {
return this.data.trim().replaceAll("(\r?\n){3,}", "\r\n\r\n");
}
static {
Lyrics.FORMATS.add(SynchronizedLyricsLRC.class);
}
}

View file

@ -0,0 +1,72 @@
package com.kabouzeid.gramophone.model.lyrics;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
class SynchronizedLyricsLRC extends AbsSynchronizedLyrics {
private static final Pattern LRC_LINE_PATTERN = Pattern.compile("((?:\\[.*?\\])+)(.*)");
private static final Pattern LRC_TIME_PATTERN = Pattern.compile("\\[(\\d+):(\\d{2}(?:\\.\\d+)?)\\]");
private static final Pattern LRC_ATTRIBUTE_PATTERN = Pattern.compile("\\[(\\D+):(.+)\\]");
private static final float LRC_SECONDS_TO_MS_MULTIPLIER = 1000f;
private static final int LRC_MINUTES_TO_MS_MULTIPLIER = 60000;
@Override
public SynchronizedLyricsLRC parse(boolean check) {
if (this.parsed || this.data == null || this.data.isEmpty()) {
return this;
}
String[] lines = this.data.split("\r?\n");
for (String line : lines) {
line = line.trim();
if (line.isEmpty()) {
continue;
}
Matcher attrMatcher = SynchronizedLyricsLRC.LRC_ATTRIBUTE_PATTERN.matcher(line);
if (attrMatcher.find()) {
try {
String attr = attrMatcher.group(1).toLowerCase().trim();
String value = attrMatcher.group(2).toLowerCase().trim();
switch (attr) {
case "offset":
this.offset = Integer.parseInt(value);
break;
}
} catch (Exception ex) {
ex.printStackTrace();
}
} else {
Matcher matcher = SynchronizedLyricsLRC.LRC_LINE_PATTERN.matcher(line);
if (matcher.find()) {
String time = matcher.group(1);
String text = matcher.group(2);
Matcher timeMatcher = SynchronizedLyricsLRC.LRC_TIME_PATTERN.matcher(time);
while (timeMatcher.find()) {
int m = 0;
float s = 0f;
try {
m = Integer.parseInt(timeMatcher.group(1));
s = Float.parseFloat(timeMatcher.group(2));
} catch (NumberFormatException ex) {
ex.printStackTrace();
}
int ms = (int) (s * LRC_SECONDS_TO_MS_MULTIPLIER) + m * LRC_MINUTES_TO_MS_MULTIPLIER;
this.valid = true;
if (check) return this;
this.lines.append(ms, text);
}
}
}
}
this.parsed = true;
return this;
}
}

View file

@ -10,13 +10,19 @@ import android.view.View;
import android.view.ViewGroup;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
import com.kabouzeid.gramophone.R;
import com.kabouzeid.gramophone.adapter.AlbumCoverPagerAdapter;
import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
import com.kabouzeid.gramophone.helper.MusicProgressViewUpdateHelper;
import com.kabouzeid.gramophone.misc.SimpleAnimatorListener;
import com.kabouzeid.gramophone.model.lyrics.AbsSynchronizedLyrics;
import com.kabouzeid.gramophone.model.lyrics.Lyrics;
import com.kabouzeid.gramophone.ui.fragments.AbsMusicServiceFragment;
import com.kabouzeid.gramophone.util.PreferenceUtil;
import com.kabouzeid.gramophone.util.ViewUtil;
import butterknife.BindView;
@ -26,9 +32,11 @@ import butterknife.Unbinder;
/**
* @author Karim Abou Zeid (kabouzeid)
*/
public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements ViewPager.OnPageChangeListener {
public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements ViewPager.OnPageChangeListener, MusicProgressViewUpdateHelper.Callback {
public static final String TAG = PlayerAlbumCoverFragment.class.getSimpleName();
public static final int LYRICS_ANIM_DURATION = 300;
private Unbinder unbinder;
@BindView(R.id.player_album_cover_viewpager)
@ -36,9 +44,19 @@ public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements
@BindView(R.id.player_favorite_icon)
ImageView favoriteIcon;
@BindView(R.id.player_lyrics)
FrameLayout lyricsLayout;
@BindView(R.id.player_lyrics_line1)
TextView lyricsLine1;
@BindView(R.id.player_lyrics_line2)
TextView lyricsLine2;
private Callbacks callbacks;
private int currentPosition;
private Lyrics lyrics;
private MusicProgressViewUpdateHelper progressViewUpdateHelper;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -68,12 +86,15 @@ public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements
return gestureDetector.onTouchEvent(event);
}
});
progressViewUpdateHelper = new MusicProgressViewUpdateHelper(this, 500, 1000);
progressViewUpdateHelper.start();
}
@Override
public void onDestroyView() {
super.onDestroyView();
viewPager.removeOnPageChangeListener(this);
progressViewUpdateHelper.stop();
unbinder.unbind();
}
@ -163,6 +184,43 @@ public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements
.start();
}
private boolean isLyricsLayoutVisible() {
return lyrics != null && lyrics.isSynchronized() && lyrics.isValid() && PreferenceUtil.getInstance(getActivity()).synchronizedLyricsShow();
}
private boolean isLyricsLayoutBound() {
return lyricsLayout != null && lyricsLine1 != null && lyricsLine2 != null;
}
private void hideLyricsLayout() {
lyricsLayout.animate().alpha(0f).setDuration(PlayerAlbumCoverFragment.LYRICS_ANIM_DURATION).withEndAction(new Runnable() {
@Override
public void run() {
if (!isLyricsLayoutBound()) return;
lyricsLayout.setVisibility(View.GONE);
lyricsLine1.setText(null);
lyricsLine2.setText(null);
}
});
}
public void setLyrics(Lyrics l) {
lyrics = l;
if (!isLyricsLayoutBound()) return;
if (!isLyricsLayoutVisible()) {
hideLyricsLayout();
return;
}
lyricsLine1.setText(null);
lyricsLine2.setText(null);
lyricsLayout.setVisibility(View.VISIBLE);
lyricsLayout.animate().alpha(1f).setDuration(PlayerAlbumCoverFragment.LYRICS_ANIM_DURATION);
}
private void notifyColorChange(int color) {
if (callbacks != null) callbacks.onColorChanged(color);
}
@ -171,6 +229,44 @@ public class PlayerAlbumCoverFragment extends AbsMusicServiceFragment implements
callbacks = listener;
}
@Override
public void onUpdateProgressViews(int progress, int total) {
if (!isLyricsLayoutBound()) return;
if (!isLyricsLayoutVisible()) {
hideLyricsLayout();
return;
}
if (!(lyrics instanceof AbsSynchronizedLyrics)) return;
AbsSynchronizedLyrics synchronizedLyrics = (AbsSynchronizedLyrics) lyrics;
lyricsLayout.setVisibility(View.VISIBLE);
lyricsLayout.setAlpha(1f);
String oldLine = lyricsLine2.getText().toString();
String line = synchronizedLyrics.getLine(progress);
if (!oldLine.equals(line) || oldLine.isEmpty()) {
lyricsLine1.setText(oldLine);
lyricsLine2.setText(line);
lyricsLine1.setVisibility(View.VISIBLE);
lyricsLine2.setVisibility(View.VISIBLE);
lyricsLine2.measure(View.MeasureSpec.makeMeasureSpec(lyricsLine2.getMeasuredWidth(), View.MeasureSpec.EXACTLY), View.MeasureSpec.UNSPECIFIED);
int h = lyricsLine2.getMeasuredHeight();
lyricsLine1.setAlpha(1f);
lyricsLine1.setTranslationY(0f);
lyricsLine1.animate().alpha(0f).translationY(-h).setDuration(PlayerAlbumCoverFragment.LYRICS_ANIM_DURATION);
lyricsLine2.setAlpha(0f);
lyricsLine2.setTranslationY(h);
lyricsLine2.animate().alpha(1f).translationY(0f).setDuration(PlayerAlbumCoverFragment.LYRICS_ANIM_DURATION);
}
}
public interface Callbacks {
void onColorChanged(int color);

View file

@ -42,6 +42,7 @@ import com.kabouzeid.gramophone.dialogs.SongShareDialog;
import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
import com.kabouzeid.gramophone.helper.menu.SongMenuHelper;
import com.kabouzeid.gramophone.model.Song;
import com.kabouzeid.gramophone.model.lyrics.Lyrics;
import com.kabouzeid.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
import com.kabouzeid.gramophone.ui.fragments.player.AbsPlayerFragment;
import com.kabouzeid.gramophone.ui.fragments.player.PlayerAlbumCoverFragment;
@ -51,11 +52,6 @@ import com.kabouzeid.gramophone.util.ViewUtil;
import com.kabouzeid.gramophone.views.WidthFitSquareLayout;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey;
import java.io.File;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
@ -93,7 +89,7 @@ public class CardPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
private AsyncTask updateIsFavoriteTask;
private AsyncTask updateLyricsAsyncTask;
private LyricsDialog.LyricInfo lyricsInfo;
private Lyrics lyrics;
private Impl impl;
@ -240,8 +236,8 @@ public class CardPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_show_lyrics:
if (lyricsInfo != null)
LyricsDialog.create(lyricsInfo).show(getFragmentManager(), "LYRICS");
if (lyrics != null)
LyricsDialog.create(lyrics).show(getFragmentManager(), "LYRICS");
return true;
}
return super.onMenuItemClick(item);
@ -304,35 +300,33 @@ public class CardPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
private void updateLyrics() {
if (updateLyricsAsyncTask != null) updateLyricsAsyncTask.cancel(false);
final Song song = MusicPlayerRemote.getCurrentSong();
updateLyricsAsyncTask = new AsyncTask<Void, Void, String>() {
updateLyricsAsyncTask = new AsyncTask<Void, Void, Lyrics>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
lyricsInfo = null;
lyrics = null;
playerAlbumCoverFragment.setLyrics(null);
toolbar.getMenu().removeItem(R.id.action_show_lyrics);
}
@Override
protected String doInBackground(Void... params) {
try {
return AudioFileIO.read(new File(song.data)).getTagOrCreateDefault().getFirst(FieldKey.LYRICS);
} catch (Exception e) {
e.printStackTrace();
cancel(false);
protected Lyrics doInBackground(Void... params) {
String data = MusicUtil.getLyrics(song);
if (TextUtils.isEmpty(data)) {
return null;
}
return Lyrics.parse(song, data);
}
@Override
protected void onPostExecute(String lyrics) {
super.onPostExecute(lyrics);
if (TextUtils.isEmpty(lyrics)) {
lyricsInfo = null;
protected void onPostExecute(Lyrics l) {
lyrics = l;
playerAlbumCoverFragment.setLyrics(lyrics);
if (lyrics == null) {
if (toolbar != null) {
toolbar.getMenu().removeItem(R.id.action_show_lyrics);
}
} else {
lyricsInfo = new LyricsDialog.LyricInfo(song.title, lyrics);
Activity activity = getActivity();
if (toolbar != null && activity != null)
if (toolbar.getMenu().findItem(R.id.action_show_lyrics) == null) {
@ -347,7 +341,7 @@ public class CardPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
}
@Override
protected void onCancelled(String s) {
protected void onCancelled(Lyrics s) {
onPostExecute(null);
}
}.execute();

View file

@ -40,6 +40,7 @@ import com.kabouzeid.gramophone.dialogs.SongShareDialog;
import com.kabouzeid.gramophone.helper.MusicPlayerRemote;
import com.kabouzeid.gramophone.helper.menu.SongMenuHelper;
import com.kabouzeid.gramophone.model.Song;
import com.kabouzeid.gramophone.model.lyrics.Lyrics;
import com.kabouzeid.gramophone.ui.activities.base.AbsSlidingMusicPanelActivity;
import com.kabouzeid.gramophone.ui.fragments.player.AbsPlayerFragment;
import com.kabouzeid.gramophone.ui.fragments.player.PlayerAlbumCoverFragment;
@ -49,11 +50,6 @@ import com.kabouzeid.gramophone.util.ViewUtil;
import com.kabouzeid.gramophone.views.WidthFitSquareLayout;
import com.sothree.slidinguppanel.SlidingUpPanelLayout;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey;
import java.io.File;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.Unbinder;
@ -90,7 +86,7 @@ public class FlatPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
private AsyncTask updateIsFavoriteTask;
private AsyncTask updateLyricsAsyncTask;
private LyricsDialog.LyricInfo lyricsInfo;
private Lyrics lyrics;
private Impl impl;
@ -236,8 +232,8 @@ public class FlatPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_show_lyrics:
if (lyricsInfo != null)
LyricsDialog.create(lyricsInfo).show(getFragmentManager(), "LYRICS");
if (lyrics != null)
LyricsDialog.create(lyrics).show(getFragmentManager(), "LYRICS");
return true;
}
return super.onMenuItemClick(item);
@ -300,35 +296,33 @@ public class FlatPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
private void updateLyrics() {
if (updateLyricsAsyncTask != null) updateLyricsAsyncTask.cancel(false);
final Song song = MusicPlayerRemote.getCurrentSong();
updateLyricsAsyncTask = new AsyncTask<Void, Void, String>() {
updateLyricsAsyncTask = new AsyncTask<Void, Void, Lyrics>() {
@Override
protected void onPreExecute() {
super.onPreExecute();
lyricsInfo = null;
lyrics = null;
playerAlbumCoverFragment.setLyrics(null);
toolbar.getMenu().removeItem(R.id.action_show_lyrics);
}
@Override
protected String doInBackground(Void... params) {
try {
return AudioFileIO.read(new File(song.data)).getTagOrCreateDefault().getFirst(FieldKey.LYRICS);
} catch (Exception e) {
e.printStackTrace();
cancel(false);
protected Lyrics doInBackground(Void... params) {
String data = MusicUtil.getLyrics(song);
if (TextUtils.isEmpty(data)) {
return null;
}
return Lyrics.parse(song, data);
}
@Override
protected void onPostExecute(String lyrics) {
super.onPostExecute(lyrics);
if (TextUtils.isEmpty(lyrics)) {
lyricsInfo = null;
protected void onPostExecute(Lyrics l) {
lyrics = l;
playerAlbumCoverFragment.setLyrics(lyrics);
if (lyrics == null) {
if (toolbar != null) {
toolbar.getMenu().removeItem(R.id.action_show_lyrics);
}
} else {
lyricsInfo = new LyricsDialog.LyricInfo(song.title, lyrics);
Activity activity = getActivity();
if (toolbar != null && activity != null)
if (toolbar.getMenu().findItem(R.id.action_show_lyrics) == null) {
@ -343,7 +337,7 @@ public class FlatPlayerFragment extends AbsPlayerFragment implements PlayerAlbum
}
@Override
protected void onCancelled(String s) {
protected void onCancelled(Lyrics s) {
onPostExecute(null);
}
}.execute();

View file

@ -11,9 +11,13 @@ import com.kabouzeid.gramophone.loader.SongLoader;
import com.kabouzeid.gramophone.loader.SortedCursor;
import com.kabouzeid.gramophone.model.Song;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
@ -160,4 +164,30 @@ public final class FileUtil {
}
return false;
}
public static String stripExtension(String str) {
if (str == null) return null;
int pos = str.lastIndexOf('.');
if (pos == -1) return str;
return str.substring(0, pos);
}
public static String readFromStream(InputStream is) throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
if (sb.length() > 0) sb.append("\n");
sb.append(line);
}
reader.close();
return sb.toString();
}
public static String read(File file) throws Exception {
FileInputStream fin = new FileInputStream(file);
String ret = readFromStream(fin);
fin.close();
return ret;
}
}

View file

@ -24,10 +24,17 @@ import com.kabouzeid.gramophone.loader.SongLoader;
import com.kabouzeid.gramophone.model.Artist;
import com.kabouzeid.gramophone.model.Playlist;
import com.kabouzeid.gramophone.model.Song;
import com.kabouzeid.gramophone.model.lyrics.AbsSynchronizedLyrics;
import org.jaudiotagger.audio.AudioFileIO;
import org.jaudiotagger.tag.FieldKey;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/**
* @author Karim Abou Zeid (kabouzeid)
@ -255,4 +262,59 @@ public class MusicUtil {
if (musicMediaTitle.isEmpty()) return "";
return String.valueOf(musicMediaTitle.charAt(0)).toUpperCase();
}
@Nullable
public static String getLyrics(Song song) {
String lyrics = null;
File file = new File(song.data);
try {
lyrics = AudioFileIO.read(file).getTagOrCreateDefault().getFirst(FieldKey.LYRICS);
} catch (Exception e) {
e.printStackTrace();
}
if (lyrics == null || lyrics.trim().isEmpty() || !AbsSynchronizedLyrics.isSynchronized(lyrics)) {
File dir = file.getAbsoluteFile().getParentFile();
if (dir != null && dir.exists() && dir.isDirectory()) {
String format = ".*%s.*\\.(lrc|txt)";
String filename = Pattern.quote(FileUtil.stripExtension(file.getName()));
String songtitle = Pattern.quote(song.title);
final ArrayList<Pattern> patterns = new ArrayList<>();
patterns.add(Pattern.compile(String.format(format, filename), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
patterns.add(Pattern.compile(String.format(format, songtitle), Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE));
File[] files = dir.listFiles(new FileFilter() {
@Override
public boolean accept(File f) {
for (Pattern pattern : patterns) {
if (pattern.matcher(f.getName()).matches()) return true;
}
return false;
}
});
if (files != null && files.length > 0) {
for (File f : files) {
try {
String newLyrics = FileUtil.read(f);
if (newLyrics != null && !newLyrics.trim().isEmpty()) {
if (AbsSynchronizedLyrics.isSynchronized(newLyrics)) {
return newLyrics;
}
lyrics = newLyrics;
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
return lyrics;
}
}

View file

@ -69,6 +69,8 @@ public final class PreferenceUtil {
public static final String START_DIRECTORY = "start_directory";
public static final String SYNCHRONIZED_LYRICS_SHOW = "synchronized_lyrics_show";
private static PreferenceUtil sInstance;
private final SharedPreferences mPreferences;
@ -400,4 +402,8 @@ public final class PreferenceUtil {
editor.putString(START_DIRECTORY, file.getPath());
editor.apply();
}
public final boolean synchronizedLyricsShow() {
return mPreferences.getBoolean(SYNCHRONIZED_LYRICS_SHOW, true);
}
}

View file

@ -10,6 +10,42 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout
android:id="@+id/player_lyrics"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:background="@drawable/shadow_up"
android:padding="16dp"
android:clipToPadding="false"
android:visibility="gone"
android:alpha="0">
<TextView
android:id="@+id/player_lyrics_line1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:textAlignment="center"
android:textSize="22sp"
android:textColor="@color/md_white_1000"
android:shadowColor="@color/md_black_1000"
android:shadowRadius="4"
android:visibility="gone" />
<TextView
android:id="@+id/player_lyrics_line2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:textAlignment="center"
android:textSize="22sp"
android:textColor="@color/md_white_1000"
android:shadowColor="@color/md_black_1000"
android:shadowRadius="4" />
</FrameLayout>
<android.support.v7.widget.AppCompatImageView
android:id="@+id/player_favorite_icon"
android:layout_width="match_parent"

View file

@ -135,6 +135,7 @@
<string name="pref_title_playback_controller_card_now_playing">Показывать подложку под кнопками</string>
<string name="pref_title_colored_playback_controls_now_playing">Окрашенные кнопки управления воспроизведением</string>
<string name="pref_title_audio_ducking">Уменьшить громкость при уведомлених</string>
<string name="pref_title_synchronized_lyrics_show">Показывать синхронизированные тексты</string>
<string name="no_equalizer">Эквалайзер не найден.</string>
<string name="no_audio_ID">"Сначала воспроизведите песню, а затем попробуйте снова."</string>
<string name="delete_action">Удалить</string>
@ -173,6 +174,7 @@
<string name="pref_summary_colored_navigation_bar">Окрашивает панель навигации в основной цвет.</string>
<string name="pref_summary_colored_app_shortcuts">Окрашивает шорткаты в основной цвет.</string>
<string name="pref_summary_audio_ducking">Уведомления, навигация, т.д.</string>
<string name="pref_summary_synchronized_lyrics_show">Показывать синхронизированные тексты песен при их наличии</string>
<string name="could_not_download_album_cover">"\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044c \u043f\u043e\u0434\u0445\u043e\u0434\u044f\u0449\u0443\u044e \u043e\u0431\u043b\u043e\u0436\u043a\u0443 \u0430\u043b\u044c\u0431\u043e\u043c\u0430."</string>
<string name="search_hint">Поиск библиотеки...</string>
<string name="rescanning_media">Повторное сканирование медиа...</string>

View file

@ -146,6 +146,7 @@
<string name="pref_title_colored_playback_controls_now_playing">Colored playback controls</string>
<string name="pref_title_audio_ducking">Reduce volume on focus loss</string>
<string name="pref_title_last_added_interval">Last added playlist interval</string>
<string name="pref_title_synchronized_lyrics_show">Show synchronized lyrics</string>
<string name="no_equalizer">No equalizer found.</string>
<string name="no_audio_ID">"Play a song first, then try again."</string>
<string name="delete_action">Delete</string>
@ -185,6 +186,7 @@
<string name="pref_summary_colored_navigation_bar">Colors the navigation bar in the primary color.</string>
<string name="pref_summary_colored_app_shortcuts">Colors the app shortcuts in the primary color.</string>
<string name="pref_summary_audio_ducking">Notifications, navigation etc.</string>
<string name="pref_summary_synchronized_lyrics_show">Show synchronized lyrics for a song if available</string>
<string name="could_not_download_album_cover">"Couldn\u2019t download a matching album cover."</string>
<string name="search_hint">Search your library…</string>
<string name="rescanning_media">Rescanning media…</string>

View file

@ -7,6 +7,12 @@
android:key="now_playing_screen_id"
android:title="@string/pref_title_now_playing_screen_appearance" />
<com.kabouzeid.appthemehelper.common.prefs.supportv7.ATESwitchPreference
android:defaultValue="true"
android:key="synchronized_lyrics_show"
android:summary="@string/pref_summary_synchronized_lyrics_show"
android:title="@string/pref_title_synchronized_lyrics_show" />
</com.kabouzeid.appthemehelper.common.prefs.supportv7.ATEPreferenceCategory>
</android.support.v7.preference.PreferenceScreen>