diff --git a/app/src/main/java/com/kabouzeid/gramophone/dialogs/LyricsDialog.java b/app/src/main/java/com/kabouzeid/gramophone/dialogs/LyricsDialog.java index 874d47e6..ed8cc2ce 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/dialogs/LyricsDialog.java +++ b/app/src/main/java/com/kabouzeid/gramophone/dialogs/LyricsDialog.java @@ -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 CREATOR = new Parcelable.Creator() { - @Override - public LyricInfo createFromParcel(Parcel source) { - return new LyricInfo(source); - } - - @Override - public LyricInfo[] newArray(int size) { - return new LyricInfo[size]; - } - }; - } } diff --git a/app/src/main/java/com/kabouzeid/gramophone/helper/MusicProgressViewUpdateHelper.java b/app/src/main/java/com/kabouzeid/gramophone/helper/MusicProgressViewUpdateHelper.java index dfa801f9..4f912e89 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/helper/MusicProgressViewUpdateHelper.java +++ b/app/src/main/java/com/kabouzeid/gramophone/helper/MusicProgressViewUpdateHelper.java @@ -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); } diff --git a/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/AbsSynchronizedLyrics.java b/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/AbsSynchronizedLyrics.java new file mode 100644 index 00000000..6ae8de9a --- /dev/null +++ b/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/AbsSynchronizedLyrics.java @@ -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 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(); + } +} diff --git a/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/Lyrics.java b/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/Lyrics.java new file mode 100644 index 00000000..fb38ca25 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/Lyrics.java @@ -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> 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 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 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); + } +} diff --git a/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/SynchronizedLyricsLRC.java b/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/SynchronizedLyricsLRC.java new file mode 100644 index 00000000..3d1e3632 --- /dev/null +++ b/app/src/main/java/com/kabouzeid/gramophone/model/lyrics/SynchronizedLyricsLRC.java @@ -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; + } +} diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/PlayerAlbumCoverFragment.java b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/PlayerAlbumCoverFragment.java index 2503281d..60cf18e3 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/PlayerAlbumCoverFragment.java +++ b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/PlayerAlbumCoverFragment.java @@ -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); diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/card/CardPlayerFragment.java b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/card/CardPlayerFragment.java index 3d002a18..c939bf1d 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/card/CardPlayerFragment.java +++ b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/card/CardPlayerFragment.java @@ -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() { + updateLyricsAsyncTask = new AsyncTask() { @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(); diff --git a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/flat/FlatPlayerFragment.java b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/flat/FlatPlayerFragment.java index 434849ba..16adf455 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/flat/FlatPlayerFragment.java +++ b/app/src/main/java/com/kabouzeid/gramophone/ui/fragments/player/flat/FlatPlayerFragment.java @@ -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() { + updateLyricsAsyncTask = new AsyncTask() { @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(); diff --git a/app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java b/app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java index 7563f12c..df54e766 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java +++ b/app/src/main/java/com/kabouzeid/gramophone/util/FileUtil.java @@ -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; + } } diff --git a/app/src/main/java/com/kabouzeid/gramophone/util/MusicUtil.java b/app/src/main/java/com/kabouzeid/gramophone/util/MusicUtil.java index 8b11a85e..656d6873 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/util/MusicUtil.java +++ b/app/src/main/java/com/kabouzeid/gramophone/util/MusicUtil.java @@ -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 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; + } } diff --git a/app/src/main/java/com/kabouzeid/gramophone/util/PreferenceUtil.java b/app/src/main/java/com/kabouzeid/gramophone/util/PreferenceUtil.java index a893218c..565df23e 100644 --- a/app/src/main/java/com/kabouzeid/gramophone/util/PreferenceUtil.java +++ b/app/src/main/java/com/kabouzeid/gramophone/util/PreferenceUtil.java @@ -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); + } } diff --git a/app/src/main/res/layout/fragment_player_album_cover.xml b/app/src/main/res/layout/fragment_player_album_cover.xml index 03c14fc4..17e13007 100644 --- a/app/src/main/res/layout/fragment_player_album_cover.xml +++ b/app/src/main/res/layout/fragment_player_album_cover.xml @@ -10,6 +10,42 @@ android:layout_width="match_parent" android:layout_height="match_parent" /> + + + + + + + + Показывать подложку под кнопками Окрашенные кнопки управления воспроизведением Уменьшить громкость при уведомлених + Показывать синхронизированные тексты Эквалайзер не найден. "Сначала воспроизведите песню, а затем попробуйте снова." Удалить @@ -173,6 +174,7 @@ Окрашивает панель навигации в основной цвет. Окрашивает шорткаты в основной цвет. Уведомления, навигация, т.д. + Показывать синхронизированные тексты песен при их наличии "\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." Поиск библиотеки... Повторное сканирование медиа... diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f50e1172..b4a69299 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -146,6 +146,7 @@ Colored playback controls Reduce volume on focus loss Last added playlist interval + Show synchronized lyrics No equalizer found. "Play a song first, then try again." Delete @@ -185,6 +186,7 @@ Colors the navigation bar in the primary color. Colors the app shortcuts in the primary color. Notifications, navigation etc. + Show synchronized lyrics for a song if available "Couldn\u2019t download a matching album cover." Search your library… Rescanning media… diff --git a/app/src/main/res/xml/pref_now_playing_screen.xml b/app/src/main/res/xml/pref_now_playing_screen.xml index 33a1f66c..83f548a1 100644 --- a/app/src/main/res/xml/pref_now_playing_screen.xml +++ b/app/src/main/res/xml/pref_now_playing_screen.xml @@ -7,6 +7,12 @@ android:key="now_playing_screen_id" android:title="@string/pref_title_now_playing_screen_appearance" /> + + \ No newline at end of file