Display transcript text and follow along the audio (#7103)
This commit is contained in:
parent
7c4f19c979
commit
e856a9f118
|
@ -69,6 +69,7 @@ dependencies {
|
||||||
implementation project(':net:ssl')
|
implementation project(':net:ssl')
|
||||||
implementation project(':net:sync:service')
|
implementation project(':net:sync:service')
|
||||||
implementation project(':parser:feed')
|
implementation project(':parser:feed')
|
||||||
|
implementation project(':parser:transcript')
|
||||||
implementation project(':playback:base')
|
implementation project(':playback:base')
|
||||||
implementation project(':playback:cast')
|
implementation project(':playback:cast')
|
||||||
implementation project(':storage:database')
|
implementation project(':storage:database')
|
||||||
|
@ -88,6 +89,7 @@ dependencies {
|
||||||
implementation project(':net:sync:service-interface')
|
implementation project(':net:sync:service-interface')
|
||||||
implementation project(':playback:service')
|
implementation project(':playback:service')
|
||||||
implementation project(':ui:chapters')
|
implementation project(':ui:chapters')
|
||||||
|
implementation project(':ui:transcript')
|
||||||
|
|
||||||
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
|
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
|
||||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||||
|
|
|
@ -61,6 +61,7 @@ public class FeedItemMenuHandler {
|
||||||
final boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists();
|
final boolean fileDownloaded = hasMedia && selectedItem.getMedia().fileExists();
|
||||||
final boolean isLocalFile = hasMedia && selectedItem.getFeed().isLocalFeed();
|
final boolean isLocalFile = hasMedia && selectedItem.getFeed().isLocalFeed();
|
||||||
final boolean isFavorite = selectedItem.isTagged(FeedItem.TAG_FAVORITE);
|
final boolean isFavorite = selectedItem.isTagged(FeedItem.TAG_FAVORITE);
|
||||||
|
final boolean hasTranscript = selectedItem.hasTranscript();
|
||||||
|
|
||||||
setItemVisibility(menu, R.id.skip_episode_item, isPlaying);
|
setItemVisibility(menu, R.id.skip_episode_item, isPlaying);
|
||||||
setItemVisibility(menu, R.id.remove_from_queue_item, isInQueue);
|
setItemVisibility(menu, R.id.remove_from_queue_item, isInQueue);
|
||||||
|
@ -85,6 +86,7 @@ public class FeedItemMenuHandler {
|
||||||
setItemVisibility(menu, R.id.add_to_favorites_item, !isFavorite);
|
setItemVisibility(menu, R.id.add_to_favorites_item, !isFavorite);
|
||||||
setItemVisibility(menu, R.id.remove_from_favorites_item, isFavorite);
|
setItemVisibility(menu, R.id.remove_from_favorites_item, isFavorite);
|
||||||
setItemVisibility(menu, R.id.remove_item, fileDownloaded || isLocalFile);
|
setItemVisibility(menu, R.id.remove_item, fileDownloaded || isLocalFile);
|
||||||
|
setItemVisibility(menu, R.id.transcript_item, hasTranscript);
|
||||||
|
|
||||||
if (selectedItem.getFeed().getState() != Feed.STATE_SUBSCRIBED) {
|
if (selectedItem.getFeed().getState() != Feed.STATE_SUBSCRIBED) {
|
||||||
setItemVisibility(menu, R.id.mark_read_item, false);
|
setItemVisibility(menu, R.id.mark_read_item, false);
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
package de.danoeh.antennapod.ui.screen.playback;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.google.android.material.elevation.SurfaceColors;
|
||||||
|
import de.danoeh.antennapod.databinding.TranscriptItemBinding;
|
||||||
|
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||||
|
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||||
|
import de.danoeh.antennapod.model.feed.TranscriptSegment;
|
||||||
|
import de.danoeh.antennapod.model.playback.Playable;
|
||||||
|
import de.danoeh.antennapod.ui.common.Converter;
|
||||||
|
import de.danoeh.antennapod.ui.transcript.TranscriptViewholder;
|
||||||
|
import java.util.Set;
|
||||||
|
import org.greenrobot.eventbus.EventBus;
|
||||||
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
|
import org.jsoup.internal.StringUtil;
|
||||||
|
|
||||||
|
public class TranscriptAdapter extends RecyclerView.Adapter<TranscriptViewholder> {
|
||||||
|
|
||||||
|
public String tag = "TranscriptAdapter";
|
||||||
|
private final SegmentClickListener segmentClickListener;
|
||||||
|
private final Context context;
|
||||||
|
private FeedMedia media;
|
||||||
|
private int prevHighlightPosition = -1;
|
||||||
|
private int highlightPosition = -1;
|
||||||
|
|
||||||
|
public TranscriptAdapter(Context context, SegmentClickListener segmentClickListener) {
|
||||||
|
this.context = context;
|
||||||
|
this.segmentClickListener = segmentClickListener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public TranscriptViewholder onCreateViewHolder(@NonNull ViewGroup viewGroup, int viewType) {
|
||||||
|
return new TranscriptViewholder(TranscriptItemBinding.inflate(LayoutInflater.from(context), viewGroup, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMedia(Playable media) {
|
||||||
|
if (!(media instanceof FeedMedia)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.media = (FeedMedia) media;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull TranscriptViewholder holder, int position) {
|
||||||
|
if (media == null || media.getTranscript() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TranscriptSegment seg = media.getTranscript().getSegmentAt(position);
|
||||||
|
holder.viewContent.setOnClickListener(v -> {
|
||||||
|
if (segmentClickListener != null) {
|
||||||
|
segmentClickListener.onTranscriptClicked(position, seg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
String timecode = Converter.getDurationStringLong((int) seg.getStartTime());
|
||||||
|
if (!StringUtil.isBlank(seg.getSpeaker())) {
|
||||||
|
if (position > 0 && media.getTranscript()
|
||||||
|
.getSegmentAt(position - 1).getSpeaker().equals(seg.getSpeaker())) {
|
||||||
|
holder.viewTimecode.setVisibility(View.GONE);
|
||||||
|
holder.viewContent.setText(seg.getWords());
|
||||||
|
} else {
|
||||||
|
holder.viewTimecode.setVisibility(View.VISIBLE);
|
||||||
|
holder.viewTimecode.setText(timecode + " • " + seg.getSpeaker());
|
||||||
|
holder.viewContent.setText(seg.getWords());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Set<String> speakers = media.getTranscript().getSpeakers();
|
||||||
|
if (speakers.isEmpty() && (position % 5 == 0)) {
|
||||||
|
holder.viewTimecode.setVisibility(View.VISIBLE);
|
||||||
|
holder.viewTimecode.setText(timecode);
|
||||||
|
}
|
||||||
|
holder.viewContent.setText(seg.getWords());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (position == highlightPosition) {
|
||||||
|
float density = context.getResources().getDisplayMetrics().density;
|
||||||
|
holder.viewContent.setBackgroundColor(SurfaceColors.getColorForElevation(context, 32 * density));
|
||||||
|
holder.viewContent.setAlpha(1.0f);
|
||||||
|
holder.viewTimecode.setAlpha(1.0f);
|
||||||
|
holder.viewContent.setAlpha(1.0f);
|
||||||
|
} else {
|
||||||
|
holder.viewContent.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent));
|
||||||
|
holder.viewContent.setAlpha(0.5f);
|
||||||
|
holder.viewTimecode.setAlpha(0.5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
|
public void onEventMainThread(PlaybackPositionEvent event) {
|
||||||
|
if (media == null || media.getTranscript() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int index = media.getTranscript().findSegmentIndexBefore(event.getPosition());
|
||||||
|
if (index < 0 || index > media.getTranscript().getSegmentCount()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (prevHighlightPosition != highlightPosition) {
|
||||||
|
prevHighlightPosition = highlightPosition;
|
||||||
|
}
|
||||||
|
if (index != highlightPosition) {
|
||||||
|
highlightPosition = index;
|
||||||
|
notifyItemChanged(prevHighlightPosition);
|
||||||
|
notifyItemChanged(highlightPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
if (media == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.getTranscript() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return media.getTranscript().getSegmentCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||||
|
super.onAttachedToRecyclerView(recyclerView);
|
||||||
|
EventBus.getDefault().register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
|
||||||
|
super.onDetachedFromRecyclerView(recyclerView);
|
||||||
|
EventBus.getDefault().unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public interface SegmentClickListener {
|
||||||
|
void onTranscriptClicked(int position, TranscriptSegment seg);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,220 @@
|
||||||
|
package de.danoeh.antennapod.ui.screen.playback;
|
||||||
|
|
||||||
|
import android.app.Dialog;
|
||||||
|
import android.content.DialogInterface;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.fragment.app.DialogFragment;
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.LinearSmoothScroller;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
import de.danoeh.antennapod.R;
|
||||||
|
import de.danoeh.antennapod.databinding.TranscriptDialogBinding;
|
||||||
|
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||||
|
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||||
|
import de.danoeh.antennapod.model.feed.Transcript;
|
||||||
|
import de.danoeh.antennapod.model.feed.TranscriptSegment;
|
||||||
|
import de.danoeh.antennapod.model.playback.Playable;
|
||||||
|
import de.danoeh.antennapod.playback.service.PlaybackController;
|
||||||
|
import de.danoeh.antennapod.ui.transcript.TranscriptUtils;
|
||||||
|
import io.reactivex.Maybe;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import org.greenrobot.eventbus.EventBus;
|
||||||
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
|
import org.greenrobot.eventbus.ThreadMode;
|
||||||
|
|
||||||
|
public class TranscriptDialogFragment extends DialogFragment {
|
||||||
|
public static final String TAG = "TranscriptFragment";
|
||||||
|
private TranscriptDialogBinding viewBinding;
|
||||||
|
private PlaybackController controller;
|
||||||
|
private Disposable disposable;
|
||||||
|
private Playable media;
|
||||||
|
private Transcript transcript;
|
||||||
|
private TranscriptAdapter adapter = null;
|
||||||
|
private boolean doInitialScroll = true;
|
||||||
|
private LinearLayoutManager layoutManager;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
ViewGroup.LayoutParams params;
|
||||||
|
params = getDialog().getWindow().getAttributes();
|
||||||
|
params.width = WindowManager.LayoutParams.MATCH_PARENT;
|
||||||
|
getDialog().getWindow().setAttributes((WindowManager.LayoutParams) params);
|
||||||
|
super.onResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||||
|
viewBinding = TranscriptDialogBinding.inflate(getLayoutInflater());
|
||||||
|
layoutManager = new LinearLayoutManager(getContext());
|
||||||
|
viewBinding.transcriptList.setLayoutManager(layoutManager);
|
||||||
|
|
||||||
|
adapter = new TranscriptAdapter(getContext(), this::transcriptClicked);
|
||||||
|
viewBinding.transcriptList.setAdapter(adapter);
|
||||||
|
viewBinding.transcriptList.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
|
||||||
|
super.onScrollStateChanged(recyclerView, newState);
|
||||||
|
if (newState == RecyclerView.SCROLL_STATE_DRAGGING) {
|
||||||
|
viewBinding.followAudioCheckbox.setChecked(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setView(viewBinding.getRoot())
|
||||||
|
.setPositiveButton(getString(R.string.close_label), null)
|
||||||
|
.setNegativeButton(getString(R.string.refresh_label), null)
|
||||||
|
.setTitle(R.string.transcript)
|
||||||
|
.create();
|
||||||
|
viewBinding.followAudioCheckbox.setChecked(true);
|
||||||
|
dialog.setOnShowListener(dialog1 -> {
|
||||||
|
dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener(v -> {
|
||||||
|
viewBinding.progLoading.setVisibility(View.VISIBLE);
|
||||||
|
v.setClickable(false);
|
||||||
|
v.setEnabled(false);
|
||||||
|
loadMediaInfo(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
viewBinding.progLoading.setVisibility(View.VISIBLE);
|
||||||
|
doInitialScroll = true;
|
||||||
|
|
||||||
|
|
||||||
|
return dialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void transcriptClicked(int pos, TranscriptSegment segment) {
|
||||||
|
long startTime = segment.getStartTime();
|
||||||
|
long endTime = segment.getEndTime();
|
||||||
|
|
||||||
|
scrollToPosition(pos);
|
||||||
|
if (!(controller.getPosition() >= startTime && controller.getPosition() <= endTime)) {
|
||||||
|
controller.seekTo((int) startTime);
|
||||||
|
} else {
|
||||||
|
controller.playPause();
|
||||||
|
}
|
||||||
|
adapter.notifyItemChanged(pos);
|
||||||
|
viewBinding.followAudioCheckbox.setChecked(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
controller = new PlaybackController(getActivity()) {
|
||||||
|
@Override
|
||||||
|
public void loadMediaInfo() {
|
||||||
|
TranscriptDialogFragment.this.loadMediaInfo(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
controller.init();
|
||||||
|
EventBus.getDefault().register(this);
|
||||||
|
loadMediaInfo(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
|
public void onEventMainThread(PlaybackPositionEvent event) {
|
||||||
|
if (transcript == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int pos = transcript.findSegmentIndexBefore(event.getPosition());
|
||||||
|
scrollToPosition(pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadMediaInfo(boolean forceRefresh) {
|
||||||
|
if (disposable != null) {
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
disposable = Maybe.create(emitter -> {
|
||||||
|
Playable media = controller.getMedia();
|
||||||
|
if (media instanceof FeedMedia) {
|
||||||
|
this.media = media;
|
||||||
|
|
||||||
|
transcript = TranscriptUtils.loadTranscript((FeedMedia) this.media, forceRefresh);
|
||||||
|
doInitialScroll = true;
|
||||||
|
((FeedMedia) this.media).setTranscript(transcript);
|
||||||
|
emitter.onSuccess(this.media);
|
||||||
|
} else {
|
||||||
|
emitter.onComplete();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(media -> onMediaChanged((Playable) media),
|
||||||
|
error -> Log.e(TAG, Log.getStackTraceString(error)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onMediaChanged(Playable media) {
|
||||||
|
if (!(media instanceof FeedMedia)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.media = media;
|
||||||
|
|
||||||
|
if (!((FeedMedia) media).hasTranscript()) {
|
||||||
|
dismiss();
|
||||||
|
Toast.makeText(getContext(), R.string.no_transcript_label, Toast.LENGTH_LONG).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
viewBinding.progLoading.setVisibility(View.GONE);
|
||||||
|
adapter.setMedia(media);
|
||||||
|
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.INVISIBLE);
|
||||||
|
if (!TextUtils.isEmpty(((FeedMedia) media).getItem().getTranscriptUrl())) {
|
||||||
|
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE).setVisibility(View.VISIBLE);
|
||||||
|
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE).setEnabled(true);
|
||||||
|
((AlertDialog) getDialog()).getButton(DialogInterface.BUTTON_NEGATIVE).setClickable(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void scrollToPosition(int pos) {
|
||||||
|
if (pos <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!viewBinding.followAudioCheckbox.isChecked() && !doInitialScroll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
doInitialScroll = false;
|
||||||
|
|
||||||
|
boolean quickScroll = Math.abs(layoutManager.findFirstVisibleItemPosition() - pos) > 5;
|
||||||
|
if (quickScroll) {
|
||||||
|
viewBinding.transcriptList.scrollToPosition(pos - 1);
|
||||||
|
// Additionally, smooth scroll, so that currently active segment is on top of screen
|
||||||
|
}
|
||||||
|
LinearSmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) {
|
||||||
|
@Override
|
||||||
|
protected int getVerticalSnapPreference() {
|
||||||
|
return LinearSmoothScroller.SNAP_TO_START;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected float calculateSpeedPerPixel(DisplayMetrics displayMetrics) {
|
||||||
|
return (quickScroll ? 200 : 1000) / (float) displayMetrics.densityDpi;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
smoothScroller.setTargetPosition(pos - 1);
|
||||||
|
layoutManager.startSmoothScroll(smoothScroller);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
if (disposable != null) {
|
||||||
|
disposable.dispose();
|
||||||
|
}
|
||||||
|
controller.release();
|
||||||
|
controller = null;
|
||||||
|
EventBus.getDefault().unregister(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -35,6 +35,7 @@ import de.danoeh.antennapod.ui.episodes.TimeSpeedConverter;
|
||||||
import de.danoeh.antennapod.ui.screen.playback.MediaPlayerErrorDialog;
|
import de.danoeh.antennapod.ui.screen.playback.MediaPlayerErrorDialog;
|
||||||
import de.danoeh.antennapod.ui.screen.playback.PlayButton;
|
import de.danoeh.antennapod.ui.screen.playback.PlayButton;
|
||||||
import de.danoeh.antennapod.ui.screen.playback.SleepTimerDialog;
|
import de.danoeh.antennapod.ui.screen.playback.SleepTimerDialog;
|
||||||
|
import de.danoeh.antennapod.ui.screen.playback.TranscriptDialogFragment;
|
||||||
import de.danoeh.antennapod.ui.screen.playback.VariableSpeedDialog;
|
import de.danoeh.antennapod.ui.screen.playback.VariableSpeedDialog;
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
import org.greenrobot.eventbus.Subscribe;
|
import org.greenrobot.eventbus.Subscribe;
|
||||||
|
@ -163,7 +164,6 @@ public class AudioPlayerFragment extends Fragment implements
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setChapterDividers(Playable media) {
|
private void setChapterDividers(Playable media) {
|
||||||
|
|
||||||
if (media == null) {
|
if (media == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -497,6 +497,10 @@ public class AudioPlayerFragment extends Fragment implements
|
||||||
if (itemId == R.id.disable_sleeptimer_item || itemId == R.id.set_sleeptimer_item) {
|
if (itemId == R.id.disable_sleeptimer_item || itemId == R.id.set_sleeptimer_item) {
|
||||||
new SleepTimerDialog().show(getChildFragmentManager(), "SleepTimerDialog");
|
new SleepTimerDialog().show(getChildFragmentManager(), "SleepTimerDialog");
|
||||||
return true;
|
return true;
|
||||||
|
} else if (itemId == R.id.transcript_item) {
|
||||||
|
new TranscriptDialogFragment().show(
|
||||||
|
getActivity().getSupportFragmentManager(), TranscriptDialogFragment.TAG);
|
||||||
|
return true;
|
||||||
} else if (itemId == R.id.open_feed_item) {
|
} else if (itemId == R.id.open_feed_item) {
|
||||||
if (feedItem != null) {
|
if (feedItem != null) {
|
||||||
openFeed(feedItem.getFeed());
|
openFeed(feedItem.getFeed());
|
||||||
|
|
|
@ -0,0 +1,23 @@
|
||||||
|
package de.danoeh.antennapod.ui.transcript;
|
||||||
|
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import de.danoeh.antennapod.databinding.TranscriptItemBinding;
|
||||||
|
|
||||||
|
public class TranscriptViewholder extends RecyclerView.ViewHolder {
|
||||||
|
public final TextView viewTimecode;
|
||||||
|
public final TextView viewContent;
|
||||||
|
|
||||||
|
public TranscriptViewholder(TranscriptItemBinding binding) {
|
||||||
|
super(binding.getRoot());
|
||||||
|
viewTimecode = binding.speaker;
|
||||||
|
viewContent = binding.content;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return super.toString() + " '" + viewContent.getText() + "'";
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/progLoading"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:indeterminateOnly="true"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/transcript_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:scrollIndicators="right"
|
||||||
|
android:scrollbarStyle="outsideInset"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:scrollbars="vertical"
|
||||||
|
android:layout_weight="1"
|
||||||
|
tools:listitem="@layout/transcript_item" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/followAudioCheckbox"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginHorizontal="16dp"
|
||||||
|
android:text="@string/transcript_follow" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/speaker"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginVertical="8dp"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:clickable="false"
|
||||||
|
android:defaultFocusHighlightEnabled="false"
|
||||||
|
android:longClickable="false"
|
||||||
|
android:textStyle="bold"
|
||||||
|
android:textColor="?android:attr/textColorPrimary" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/content"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minLines="1"
|
||||||
|
android:maxLines="100"
|
||||||
|
android:nestedScrollingEnabled="false"
|
||||||
|
android:paddingHorizontal="24dp"
|
||||||
|
android:selectAllOnFocus="false"
|
||||||
|
android:singleLine="false"
|
||||||
|
android:textColor="?android:attr/textColorPrimary"
|
||||||
|
android:textIsSelectable="false" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
|
@ -2,6 +2,12 @@
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:custom="http://schemas.android.com/apk/res-auto">
|
xmlns:custom="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/transcript_item"
|
||||||
|
android:icon="@drawable/transcript"
|
||||||
|
android:title="@string/show_transcript"
|
||||||
|
custom:showAsAction="ifRoom">
|
||||||
|
</item>
|
||||||
<item
|
<item
|
||||||
android:id="@+id/add_to_favorites_item"
|
android:id="@+id/add_to_favorites_item"
|
||||||
android:icon="@drawable/ic_star_border"
|
android:icon="@drawable/ic_star_border"
|
||||||
|
|
|
@ -171,7 +171,7 @@ public class FeedItem implements Serializable {
|
||||||
if (other.podcastIndexChapterUrl != null) {
|
if (other.podcastIndexChapterUrl != null) {
|
||||||
podcastIndexChapterUrl = other.podcastIndexChapterUrl;
|
podcastIndexChapterUrl = other.podcastIndexChapterUrl;
|
||||||
}
|
}
|
||||||
if (other.getPodcastIndexTranscriptUrl() != null) {
|
if (other.getTranscriptUrl() != null) {
|
||||||
podcastIndexTranscriptUrl = other.podcastIndexTranscriptUrl;
|
podcastIndexTranscriptUrl = other.podcastIndexTranscriptUrl;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -425,15 +425,15 @@ public class FeedItem implements Serializable {
|
||||||
podcastIndexChapterUrl = url;
|
podcastIndexChapterUrl = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPodcastIndexTranscriptUrl(String type, String url) {
|
public void setTranscriptUrl(String type, String url) {
|
||||||
updateTranscriptPreferredFormat(type, url);
|
updateTranscriptPreferredFormat(type, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPodcastIndexTranscriptUrl() {
|
public String getTranscriptUrl() {
|
||||||
return podcastIndexTranscriptUrl;
|
return podcastIndexTranscriptUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPodcastIndexTranscriptType() {
|
public String getTranscriptType() {
|
||||||
return podcastIndexTranscriptType;
|
return podcastIndexTranscriptType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -459,7 +459,6 @@ public class FeedItem implements Serializable {
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
System.out.println("Invalid format for transcript " + type);
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -480,6 +479,10 @@ public class FeedItem implements Serializable {
|
||||||
return podcastIndexTranscriptText = str;
|
return podcastIndexTranscriptText = str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean hasTranscript() {
|
||||||
|
return (podcastIndexTranscriptUrl != null);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.net.Uri;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
import android.support.v4.media.MediaBrowserCompat;
|
import android.support.v4.media.MediaBrowserCompat;
|
||||||
import android.support.v4.media.MediaDescriptionCompat;
|
import android.support.v4.media.MediaDescriptionCompat;
|
||||||
import de.danoeh.antennapod.model.MediaMetadataRetrieverCompat;
|
import de.danoeh.antennapod.model.MediaMetadataRetrieverCompat;
|
||||||
|
@ -520,4 +521,25 @@ public class FeedMedia implements Playable {
|
||||||
}
|
}
|
||||||
return getLocalFileUrl() + ".transcript";
|
return getLocalFileUrl() + ".transcript";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTranscript(Transcript t) {
|
||||||
|
if (item == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
item.setTranscript(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Transcript getTranscript() {
|
||||||
|
if (item == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return item.getTranscript();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean hasTranscript() {
|
||||||
|
if (item == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return item.hasTranscript();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,28 +1,50 @@
|
||||||
package de.danoeh.antennapod.model.feed;
|
package de.danoeh.antennapod.model.feed;
|
||||||
|
|
||||||
import java.util.Map;
|
import java.util.ArrayList;
|
||||||
import java.util.TreeMap;
|
import java.util.Set;
|
||||||
|
|
||||||
public class Transcript {
|
public class Transcript {
|
||||||
|
private Set<String> speakers;
|
||||||
private final TreeMap<Long, TranscriptSegment> segmentsMap = new TreeMap<>();
|
private final ArrayList<TranscriptSegment> segments = new ArrayList<>();
|
||||||
|
|
||||||
public void addSegment(TranscriptSegment segment) {
|
public void addSegment(TranscriptSegment segment) {
|
||||||
segmentsMap.put(segment.getStartTime(), segment);
|
if ((!segments.isEmpty() && segments.get(segments.size() - 1).getStartTime() >= segment.getStartTime())) {
|
||||||
|
throw new IllegalArgumentException("Segments must be added in sorted order");
|
||||||
|
}
|
||||||
|
segments.add(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int findSegmentIndexBefore(long time) {
|
||||||
|
int a = 0;
|
||||||
|
int b = segments.size() - 1;
|
||||||
|
while (a < b) {
|
||||||
|
int pivot = (a + b + 1) / 2;
|
||||||
|
if (segments.get(pivot).getStartTime() > time) {
|
||||||
|
b = pivot - 1;
|
||||||
|
} else {
|
||||||
|
a = pivot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TranscriptSegment getSegmentAt(int index) {
|
||||||
|
return segments.get(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
public TranscriptSegment getSegmentAtTime(long time) {
|
public TranscriptSegment getSegmentAtTime(long time) {
|
||||||
if (segmentsMap.floorEntry(time) == null) {
|
return getSegmentAt(findSegmentIndexBefore(time));
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return segmentsMap.floorEntry(time).getValue();
|
|
||||||
|
public Set<String> getSpeakers() {
|
||||||
|
return speakers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSpeakers(Set<String> speakers) {
|
||||||
|
this.speakers = speakers;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getSegmentCount() {
|
public int getSegmentCount() {
|
||||||
return segmentsMap.size();
|
return segments.size();
|
||||||
}
|
|
||||||
|
|
||||||
public Map.Entry<Long, TranscriptSegment> getEntryAfterTime(long time) {
|
|
||||||
return segmentsMap.ceilingEntry(time);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ dependencies {
|
||||||
implementation project(':storage:preferences')
|
implementation project(':storage:preferences')
|
||||||
implementation project(':ui:app-start-intent')
|
implementation project(':ui:app-start-intent')
|
||||||
implementation project(':ui:chapters')
|
implementation project(':ui:chapters')
|
||||||
|
implementation project(':ui:transcript')
|
||||||
|
|
||||||
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
|
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
|
||||||
implementation "androidx.core:core:$coreVersion"
|
implementation "androidx.core:core:$coreVersion"
|
||||||
|
|
|
@ -10,7 +10,6 @@ import de.danoeh.antennapod.model.MediaMetadataRetrieverCompat;
|
||||||
import de.danoeh.antennapod.model.feed.Feed;
|
import de.danoeh.antennapod.model.feed.Feed;
|
||||||
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink;
|
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueueSink;
|
||||||
import de.danoeh.antennapod.ui.chapters.ChapterUtils;
|
import de.danoeh.antennapod.ui.chapters.ChapterUtils;
|
||||||
import de.danoeh.antennapod.ui.chapters.PodcastIndexTranscriptUtils;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.greenrobot.eventbus.EventBus;
|
import org.greenrobot.eventbus.EventBus;
|
||||||
|
|
||||||
|
@ -27,6 +26,7 @@ import de.danoeh.antennapod.model.download.DownloadError;
|
||||||
import de.danoeh.antennapod.model.feed.FeedItem;
|
import de.danoeh.antennapod.model.feed.FeedItem;
|
||||||
import de.danoeh.antennapod.model.feed.FeedMedia;
|
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||||
import de.danoeh.antennapod.net.sync.serviceinterface.EpisodeAction;
|
import de.danoeh.antennapod.net.sync.serviceinterface.EpisodeAction;
|
||||||
|
import de.danoeh.antennapod.ui.transcript.TranscriptUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a completed media download.
|
* Handles a completed media download.
|
||||||
|
@ -67,12 +67,11 @@ public class MediaDownloadedHandler implements Runnable {
|
||||||
ChapterUtils.loadChaptersFromUrl(media.getItem().getPodcastIndexChapterUrl(), false);
|
ChapterUtils.loadChaptersFromUrl(media.getItem().getPodcastIndexChapterUrl(), false);
|
||||||
}
|
}
|
||||||
FeedItem item = media.getItem();
|
FeedItem item = media.getItem();
|
||||||
if (item != null && item.getPodcastIndexTranscriptUrl() != null) {
|
if (item != null && item.getTranscriptUrl() != null) {
|
||||||
String transcript = PodcastIndexTranscriptUtils.loadTranscriptFromUrl(
|
String transcript = TranscriptUtils.loadTranscriptFromUrl(item.getTranscriptUrl(), true);
|
||||||
item.getPodcastIndexTranscriptType(), item.getPodcastIndexTranscriptUrl(), false);
|
|
||||||
if (!StringUtils.isEmpty(transcript)) {
|
if (!StringUtils.isEmpty(transcript)) {
|
||||||
item.setPodcastIndexTranscriptText(transcript);
|
item.setPodcastIndexTranscriptText(transcript);
|
||||||
PodcastIndexTranscriptUtils.storeTranscript(media, transcript);
|
TranscriptUtils.storeTranscript(media, transcript);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedIOException ignore) {
|
} catch (InterruptedIOException ignore) {
|
||||||
|
|
|
@ -34,7 +34,7 @@ public class PodcastIndex extends Namespace {
|
||||||
String href = attributes.getValue(URL);
|
String href = attributes.getValue(URL);
|
||||||
String type = attributes.getValue(TYPE);
|
String type = attributes.getValue(TYPE);
|
||||||
if (!TextUtils.isEmpty(href) && !TextUtils.isEmpty(type)) {
|
if (!TextUtils.isEmpty(href) && !TextUtils.isEmpty(type)) {
|
||||||
state.getCurrentItem().setPodcastIndexTranscriptUrl(type, href);
|
state.getCurrentItem().setTranscriptUrl(type, href);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new SyndElement(localName, this);
|
return new SyndElement(localName, this);
|
||||||
|
|
|
@ -101,8 +101,8 @@ public class RssParserTest {
|
||||||
public void testPodcastIndexTranscript() throws Exception {
|
public void testPodcastIndexTranscript() throws Exception {
|
||||||
File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testPodcastIndexTranscript.xml");
|
File feedFile = FeedParserTestHelper.getFeedFile("feed-rss-testPodcastIndexTranscript.xml");
|
||||||
Feed feed = FeedParserTestHelper.runFeedParser(feedFile);
|
Feed feed = FeedParserTestHelper.runFeedParser(feedFile);
|
||||||
assertEquals("https://podnews.net/audio/podnews231011.mp3.json", feed.getItems().get(0).getPodcastIndexTranscriptUrl());
|
assertEquals("https://podnews.net/audio/podnews231011.mp3.json", feed.getItems().get(0).getTranscriptUrl());
|
||||||
assertEquals("application/json", feed.getItems().get(0).getPodcastIndexTranscriptType());
|
assertEquals("application/json", feed.getItems().get(0).getTranscriptType());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -2,9 +2,13 @@ package de.danoeh.antennapod.parser.transcript;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.json.JSONArray;
|
import org.json.JSONArray;
|
||||||
|
import org.json.JSONException;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import org.jsoup.internal.StringUtil;
|
import org.jsoup.internal.StringUtil;
|
||||||
|
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import de.danoeh.antennapod.model.feed.Transcript;
|
import de.danoeh.antennapod.model.feed.Transcript;
|
||||||
import de.danoeh.antennapod.model.feed.TranscriptSegment;
|
import de.danoeh.antennapod.model.feed.TranscriptSegment;
|
||||||
|
|
||||||
|
@ -15,14 +19,25 @@ public class JsonTranscriptParser {
|
||||||
long startTime = -1L;
|
long startTime = -1L;
|
||||||
long endTime = -1L;
|
long endTime = -1L;
|
||||||
long segmentStartTime = -1L;
|
long segmentStartTime = -1L;
|
||||||
|
long segmentEndTime = -1L;
|
||||||
long duration = 0L;
|
long duration = 0L;
|
||||||
String speaker = "";
|
String speaker = "";
|
||||||
|
String prevSpeaker = "";
|
||||||
String segmentBody = "";
|
String segmentBody = "";
|
||||||
|
JSONArray objSegments;
|
||||||
|
Set<String> speakers = new HashSet<>();
|
||||||
|
|
||||||
|
try {
|
||||||
JSONObject obj = new JSONObject(jsonStr);
|
JSONObject obj = new JSONObject(jsonStr);
|
||||||
JSONArray objSegments = obj.getJSONArray("segments");
|
objSegments = obj.getJSONArray("segments");
|
||||||
|
} catch (JSONException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < objSegments.length(); i++) {
|
for (int i = 0; i < objSegments.length(); i++) {
|
||||||
JSONObject jsonObject = objSegments.getJSONObject(i);
|
JSONObject jsonObject = objSegments.getJSONObject(i);
|
||||||
|
segmentEndTime = endTime;
|
||||||
startTime = Double.valueOf(jsonObject.optDouble("startTime", -1) * 1000L).longValue();
|
startTime = Double.valueOf(jsonObject.optDouble("startTime", -1) * 1000L).longValue();
|
||||||
endTime = Double.valueOf(jsonObject.optDouble("endTime", -1) * 1000L).longValue();
|
endTime = Double.valueOf(jsonObject.optDouble("endTime", -1) * 1000L).longValue();
|
||||||
if (startTime < 0 || endTime < 0) {
|
if (startTime < 0 || endTime < 0) {
|
||||||
|
@ -33,11 +48,40 @@ public class JsonTranscriptParser {
|
||||||
}
|
}
|
||||||
duration += endTime - startTime;
|
duration += endTime - startTime;
|
||||||
|
|
||||||
|
prevSpeaker = speaker;
|
||||||
speaker = jsonObject.optString("speaker");
|
speaker = jsonObject.optString("speaker");
|
||||||
|
speakers.add(speaker);
|
||||||
|
if (StringUtils.isEmpty(speaker) && StringUtils.isNotEmpty(prevSpeaker)) {
|
||||||
|
speaker = prevSpeaker;
|
||||||
|
}
|
||||||
String body = jsonObject.optString("body");
|
String body = jsonObject.optString("body");
|
||||||
segmentBody += body + " ";
|
if (!prevSpeaker.equals(speaker)) {
|
||||||
|
if (StringUtils.isNotEmpty(segmentBody)) {
|
||||||
|
segmentBody = StringUtils.trim(segmentBody);
|
||||||
|
transcript.addSegment(new TranscriptSegment(segmentStartTime,
|
||||||
|
segmentEndTime,
|
||||||
|
segmentBody,
|
||||||
|
prevSpeaker));
|
||||||
|
segmentStartTime = startTime;
|
||||||
|
segmentBody = body.toString();
|
||||||
|
duration = 0L;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentBody += " " + body;
|
||||||
|
|
||||||
if (duration >= TranscriptParser.MIN_SPAN) {
|
if (duration >= TranscriptParser.MIN_SPAN) {
|
||||||
|
// Look ahead and make sure the next segment does not start with an alphanumeric character
|
||||||
|
if ((i + 1) < objSegments.length()) {
|
||||||
|
String nextSegmentFirstChar = objSegments.getJSONObject(i + 1)
|
||||||
|
.optString("body")
|
||||||
|
.substring(0, 1);
|
||||||
|
if (!StringUtils.isAlphanumeric(nextSegmentFirstChar)
|
||||||
|
&& (duration < TranscriptParser.MAX_SPAN)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
segmentBody = StringUtils.trim(segmentBody);
|
segmentBody = StringUtils.trim(segmentBody);
|
||||||
transcript.addSegment(new TranscriptSegment(segmentStartTime, endTime, segmentBody, speaker));
|
transcript.addSegment(new TranscriptSegment(segmentStartTime, endTime, segmentBody, speaker));
|
||||||
duration = 0L;
|
duration = 0L;
|
||||||
|
@ -52,12 +96,13 @@ public class JsonTranscriptParser {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (transcript.getSegmentCount() > 0) {
|
if (transcript.getSegmentCount() > 0) {
|
||||||
|
transcript.setSpeakers(speakers);
|
||||||
return transcript;
|
return transcript;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (org.json.JSONException e) {
|
} catch (JSONException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -4,8 +4,10 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
import org.jsoup.internal.StringUtil;
|
import org.jsoup.internal.StringUtil;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
@ -25,21 +27,26 @@ public class SrtTranscriptParser {
|
||||||
List<String> lines = Arrays.asList(str.split("\n"));
|
List<String> lines = Arrays.asList(str.split("\n"));
|
||||||
Iterator<String> iter = lines.iterator();
|
Iterator<String> iter = lines.iterator();
|
||||||
String speaker = "";
|
String speaker = "";
|
||||||
StringBuilder body = new StringBuilder();
|
String prevSpeaker = "";
|
||||||
|
StringBuilder body;
|
||||||
String line;
|
String line;
|
||||||
String segmentBody = "";
|
String segmentBody = "";
|
||||||
long startTimecode = -1L;
|
long startTimecode = -1L;
|
||||||
long spanStartTimecode = -1L;
|
long spanStartTimecode = -1L;
|
||||||
|
long spanEndTimecode = -1L;
|
||||||
long endTimecode = -1L;
|
long endTimecode = -1L;
|
||||||
long duration = 0L;
|
long duration = 0L;
|
||||||
|
Set<String> speakers = new HashSet<>();
|
||||||
|
|
||||||
while (iter.hasNext()) {
|
while (iter.hasNext()) {
|
||||||
|
body = new StringBuilder();
|
||||||
line = iter.next();
|
line = iter.next();
|
||||||
|
|
||||||
if (line.isEmpty()) {
|
if (line.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
spanEndTimecode = endTimecode;
|
||||||
if (line.contains("-->")) {
|
if (line.contains("-->")) {
|
||||||
String[] timecodes = line.split("-->");
|
String[] timecodes = line.split("-->");
|
||||||
if (timecodes.length < 2) {
|
if (timecodes.length < 2) {
|
||||||
|
@ -65,45 +72,53 @@ public class SrtTranscriptParser {
|
||||||
} while (iter.hasNext());
|
} while (iter.hasNext());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (body.indexOf(":") != -1) {
|
if (body.indexOf(": ") != -1) {
|
||||||
String [] parts = body.toString().trim().split(":");
|
String[] parts = body.toString().trim().split(":");
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
prevSpeaker = speaker;
|
||||||
speaker = parts[0];
|
speaker = parts[0];
|
||||||
|
speakers.add(speaker);
|
||||||
body = new StringBuilder(parts[1].strip());
|
body = new StringBuilder(parts[1].strip());
|
||||||
|
if (StringUtils.isNotEmpty(prevSpeaker) && !StringUtils.equals(speaker, prevSpeaker)) {
|
||||||
|
if (StringUtils.isNotEmpty(segmentBody)) {
|
||||||
|
transcript.addSegment(new TranscriptSegment(spanStartTimecode,
|
||||||
|
spanEndTimecode, segmentBody, prevSpeaker));
|
||||||
|
duration = 0L;
|
||||||
|
spanStartTimecode = startTimecode;
|
||||||
|
segmentBody = body.toString();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
if (!StringUtil.isBlank(body.toString())) {
|
}
|
||||||
|
} else {
|
||||||
|
if (StringUtils.isNotEmpty(prevSpeaker) && StringUtils.isEmpty(speaker)) {
|
||||||
|
speaker = prevSpeaker;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
segmentBody += " " + body;
|
segmentBody += " " + body;
|
||||||
segmentBody = StringUtils.trim(segmentBody);
|
segmentBody = StringUtils.trim(segmentBody);
|
||||||
if (duration >= TranscriptParser.MIN_SPAN && endTimecode > spanStartTimecode) {
|
if (duration >= TranscriptParser.MIN_SPAN && endTimecode > spanStartTimecode) {
|
||||||
transcript.addSegment(new TranscriptSegment(spanStartTimecode,
|
transcript.addSegment(new TranscriptSegment(spanStartTimecode, endTimecode, segmentBody, speaker));
|
||||||
endTimecode,
|
|
||||||
segmentBody,
|
|
||||||
speaker));
|
|
||||||
duration = 0L;
|
duration = 0L;
|
||||||
spanStartTimecode = -1L;
|
spanStartTimecode = -1L;
|
||||||
segmentBody = "";
|
segmentBody = "";
|
||||||
}
|
}
|
||||||
body = new StringBuilder();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StringUtil.isBlank(segmentBody) && endTimecode > spanStartTimecode) {
|
if (!StringUtil.isBlank(segmentBody) && endTimecode > spanStartTimecode) {
|
||||||
segmentBody = StringUtils.trim(segmentBody);
|
segmentBody = StringUtils.trim(segmentBody);
|
||||||
transcript.addSegment(new TranscriptSegment(spanStartTimecode,
|
transcript.addSegment(new TranscriptSegment(spanStartTimecode, endTimecode, segmentBody, speaker));
|
||||||
endTimecode,
|
|
||||||
segmentBody,
|
|
||||||
speaker));
|
|
||||||
}
|
}
|
||||||
if (transcript.getSegmentCount() > 0) {
|
if (transcript.getSegmentCount() > 0) {
|
||||||
|
transcript.setSpeakers(speakers);
|
||||||
return transcript;
|
return transcript;
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Time format 00:00:00,000
|
|
||||||
static long parseTimecode(String timecode) {
|
static long parseTimecode(String timecode) {
|
||||||
Matcher matcher = TIMECODE_PATTERN.matcher(timecode);
|
Matcher matcher = TIMECODE_PATTERN.matcher(timecode);
|
||||||
if (!matcher.matches()) {
|
if (!matcher.matches()) {
|
||||||
|
|
|
@ -5,7 +5,8 @@ import org.apache.commons.lang3.StringUtils;
|
||||||
import de.danoeh.antennapod.model.feed.Transcript;
|
import de.danoeh.antennapod.model.feed.Transcript;
|
||||||
|
|
||||||
public class TranscriptParser {
|
public class TranscriptParser {
|
||||||
static final long MIN_SPAN = 1000L; // merge short segments together to form a span of 1 second
|
static final long MIN_SPAN = 5000L; // Merge short segments together to form a span of 5 seconds
|
||||||
|
static final long MAX_SPAN = 8000L; // Don't go beyond 10 seconds when merging
|
||||||
|
|
||||||
public static Transcript parse(String str, String type) {
|
public static Transcript parse(String str, String type) {
|
||||||
if (str == null || StringUtils.isBlank(str)) {
|
if (str == null || StringUtils.isBlank(str)) {
|
||||||
|
|
|
@ -14,20 +14,20 @@ public class JsonTranscriptParserTest {
|
||||||
+ "'segments': [ "
|
+ "'segments': [ "
|
||||||
+ "{ 'speaker' : 'John Doe', 'startTime': 0.8, 'endTime': 1.9, 'body': 'And' },"
|
+ "{ 'speaker' : 'John Doe', 'startTime': 0.8, 'endTime': 1.9, 'body': 'And' },"
|
||||||
+ "{ 'speaker' : 'Sally Green', 'startTime': 1.91, 'endTime': 2.8, 'body': 'this merges' },"
|
+ "{ 'speaker' : 'Sally Green', 'startTime': 1.91, 'endTime': 2.8, 'body': 'this merges' },"
|
||||||
+ "{ 'startTime': 2.9, 'endTime': 3.4, 'body': 'the' },"
|
+ "{ 'startTime': 2.9, 'endTime': 3.4, 'body': ' the' },"
|
||||||
+ "{ 'startTime': 3.5, 'endTime': 3.6, 'body': 'person' }]}";
|
+ "{ 'startTime': 3.5, 'endTime': 3.6, 'body': ' person' }]}";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParseJson() {
|
public void testParseJson() {
|
||||||
Transcript result = JsonTranscriptParser.parse(jsonStr);
|
Transcript result = JsonTranscriptParser.parse(jsonStr);
|
||||||
|
// TODO: for gaps in the transcript (ads, music) should we return null?
|
||||||
assertEquals(result.getSegmentAtTime(0L), null);
|
assertEquals(result.getSegmentAtTime(0L).getStartTime(), 800L);
|
||||||
assertEquals(result.getSegmentAtTime(800L).getSpeaker(), "John Doe");
|
assertEquals(result.getSegmentAtTime(800L).getSpeaker(), "John Doe");
|
||||||
assertEquals(result.getSegmentAtTime(800L).getStartTime(), 800L);
|
assertEquals(result.getSegmentAtTime(800L).getStartTime(), 800L);
|
||||||
assertEquals(result.getSegmentAtTime(800L).getEndTime(), 1900L);
|
assertEquals(result.getSegmentAtTime(800L).getEndTime(), 1900L);
|
||||||
assertEquals(1910L, (long) result.getEntryAfterTime(1800L).getKey());
|
assertEquals(result.getSegmentAtTime(1800L).getStartTime(), 800L);
|
||||||
// 2 segments get merged into at least 1 second
|
// 2 segments get merged into at least 5 second
|
||||||
assertEquals("this merges the", result.getEntryAfterTime(1800L).getValue().getWords());
|
assertEquals(result.getSegmentAtTime(1800L).getWords(), "And");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -10,13 +10,13 @@ import de.danoeh.antennapod.model.feed.Transcript;
|
||||||
@RunWith(RobolectricTestRunner.class)
|
@RunWith(RobolectricTestRunner.class)
|
||||||
public class SrtTranscriptParserTest {
|
public class SrtTranscriptParserTest {
|
||||||
private static String srtStr = "1\n"
|
private static String srtStr = "1\n"
|
||||||
+ "00:00:00,000 --> 00:00:02,730\n"
|
+ "00:00:00,000 --> 00:00:50,730\n"
|
||||||
+ "John Doe: Promoting your podcast in a new\n\n"
|
+ "John Doe: Promoting your podcast in a new\n\n"
|
||||||
+ "2\n"
|
+ "2\n"
|
||||||
+ "00:00:02,730 --> 00:00:04,600\n"
|
+ "00:00:90,740 --> 00:00:91,600\n"
|
||||||
+ "way. The latest from PogNews.\n\n"
|
+ "way. The latest from PogNews.\n\n"
|
||||||
+ "00:00:04,730 --> 00:00:05,600\n"
|
+ "00:00:91,730 --> 00:00:93,600\n"
|
||||||
+ "way. The latest from PogNews.";
|
+ "We bring your favorite podcast.";
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParseSrt() {
|
public void testParseSrt() {
|
||||||
|
@ -25,9 +25,10 @@ public class SrtTranscriptParserTest {
|
||||||
assertEquals(result.getSegmentAtTime(0L).getWords(), "Promoting your podcast in a new");
|
assertEquals(result.getSegmentAtTime(0L).getWords(), "Promoting your podcast in a new");
|
||||||
assertEquals(result.getSegmentAtTime(0L).getSpeaker(), "John Doe");
|
assertEquals(result.getSegmentAtTime(0L).getSpeaker(), "John Doe");
|
||||||
assertEquals(result.getSegmentAtTime(0L).getStartTime(), 0L);
|
assertEquals(result.getSegmentAtTime(0L).getStartTime(), 0L);
|
||||||
assertEquals(result.getSegmentAtTime(0L).getEndTime(), 2730L);
|
assertEquals(result.getSegmentAtTime(0L).getEndTime(), 50730L);
|
||||||
assertEquals((long) result.getEntryAfterTime(1000L).getKey(), 2730L);
|
assertEquals(result.getSegmentAtTime(90740).getStartTime(), 90740);
|
||||||
assertEquals(result.getEntryAfterTime(1000L).getValue().getWords(), "way. The latest from PogNews.");
|
assertEquals("way. The latest from PogNews. We bring your favorite podcast.",
|
||||||
|
result.getSegmentAtTime(90740).getWords());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -52,3 +52,4 @@ include ':ui:notifications'
|
||||||
include ':ui:preferences'
|
include ':ui:preferences'
|
||||||
include ':ui:statistics'
|
include ':ui:statistics'
|
||||||
include ':ui:widget'
|
include ':ui:widget'
|
||||||
|
include ':ui:transcript'
|
||||||
|
|
|
@ -682,8 +682,8 @@ public class PodDBAdapter {
|
||||||
values.put(KEY_PODCASTINDEX_CHAPTER_URL, item.getPodcastIndexChapterUrl());
|
values.put(KEY_PODCASTINDEX_CHAPTER_URL, item.getPodcastIndexChapterUrl());
|
||||||
|
|
||||||
// We only store one transcript url, we prefer JSON if it exists
|
// We only store one transcript url, we prefer JSON if it exists
|
||||||
String type = item.getPodcastIndexTranscriptType();
|
String type = item.getTranscriptType();
|
||||||
String url = item.getPodcastIndexTranscriptUrl();
|
String url = item.getTranscriptUrl();
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
values.put(KEY_PODCASTINDEX_TRANSCRIPT_TYPE, type);
|
values.put(KEY_PODCASTINDEX_TRANSCRIPT_TYPE, type);
|
||||||
values.put(KEY_PODCASTINDEX_TRANSCRIPT_URL, url);
|
values.put(KEY_PODCASTINDEX_TRANSCRIPT_URL, url);
|
||||||
|
|
|
@ -26,8 +26,8 @@ public class FeedItemCursor extends CursorWrapper {
|
||||||
private final int indexImageUrl;
|
private final int indexImageUrl;
|
||||||
private final int indexPodcastIndexChapterUrl;
|
private final int indexPodcastIndexChapterUrl;
|
||||||
private final int indexMediaId;
|
private final int indexMediaId;
|
||||||
private final int indexPodcastIndexTranscriptUrl;
|
|
||||||
private final int indexPodcastIndexTranscriptType;
|
private final int indexPodcastIndexTranscriptType;
|
||||||
|
private final int indexPodcastIndexTranscriptUrl;
|
||||||
|
|
||||||
public FeedItemCursor(Cursor cursor) {
|
public FeedItemCursor(Cursor cursor) {
|
||||||
super(new FeedMediaCursor(cursor));
|
super(new FeedMediaCursor(cursor));
|
||||||
|
@ -45,8 +45,8 @@ public class FeedItemCursor extends CursorWrapper {
|
||||||
indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
|
indexImageUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_IMAGE_URL);
|
||||||
indexPodcastIndexChapterUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL);
|
indexPodcastIndexChapterUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_CHAPTER_URL);
|
||||||
indexMediaId = cursor.getColumnIndexOrThrow(PodDBAdapter.SELECT_KEY_MEDIA_ID);
|
indexMediaId = cursor.getColumnIndexOrThrow(PodDBAdapter.SELECT_KEY_MEDIA_ID);
|
||||||
indexPodcastIndexTranscriptUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_TRANSCRIPT_URL);
|
|
||||||
indexPodcastIndexTranscriptType = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_TRANSCRIPT_TYPE);
|
indexPodcastIndexTranscriptType = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_TRANSCRIPT_TYPE);
|
||||||
|
indexPodcastIndexTranscriptUrl = cursor.getColumnIndexOrThrow(PodDBAdapter.KEY_PODCASTINDEX_TRANSCRIPT_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -67,8 +67,8 @@ public class FeedItemCursor extends CursorWrapper {
|
||||||
getString(indexItemIdentifier),
|
getString(indexItemIdentifier),
|
||||||
getLong(indexAutoDownload) > 0,
|
getLong(indexAutoDownload) > 0,
|
||||||
getString(indexPodcastIndexChapterUrl),
|
getString(indexPodcastIndexChapterUrl),
|
||||||
getString(indexPodcastIndexTranscriptUrl),
|
getString(indexPodcastIndexTranscriptType),
|
||||||
getString(indexPodcastIndexTranscriptType));
|
getString(indexPodcastIndexTranscriptUrl));
|
||||||
if (!isNull(indexMediaId)) {
|
if (!isNull(indexMediaId)) {
|
||||||
item.setMedia(feedMediaCursor.getFeedMedia());
|
item.setMedia(feedMediaCursor.getFeedMedia());
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,11 @@ dependencies {
|
||||||
implementation project(':net:common')
|
implementation project(':net:common')
|
||||||
implementation project(':parser:media')
|
implementation project(':parser:media')
|
||||||
implementation project(':parser:feed')
|
implementation project(':parser:feed')
|
||||||
|
implementation project(':parser:transcript')
|
||||||
implementation project(':storage:database')
|
implementation project(':storage:database')
|
||||||
|
|
||||||
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
|
annotationProcessor "androidx.annotation:annotation:$annotationVersion"
|
||||||
implementation "commons-io:commons-io:$commonsioVersion"
|
implementation "commons-io:commons-io:$commonsioVersion"
|
||||||
|
implementation "org.apache.commons:commons-lang3:$commonslangVersion"
|
||||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
package de.danoeh.antennapod.ui.chapters;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
import de.danoeh.antennapod.model.feed.FeedMedia;
|
|
||||||
import de.danoeh.antennapod.net.common.AntennapodHttpClient;
|
|
||||||
import okhttp3.Request;
|
|
||||||
import okhttp3.Response;
|
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
|
|
||||||
public class PodcastIndexTranscriptUtils {
|
|
||||||
|
|
||||||
private static final String TAG = "PodcastIndexTranscript";
|
|
||||||
|
|
||||||
public static String loadTranscriptFromUrl(String type, String url, boolean forceRefresh) {
|
|
||||||
StringBuilder str = new StringBuilder();
|
|
||||||
Response response = null;
|
|
||||||
try {
|
|
||||||
Log.d(TAG, "Downloading transcript URL " + url.toString());
|
|
||||||
Request request = new Request.Builder().url(url).build();
|
|
||||||
response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
|
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
|
||||||
str.append(response.body().string());
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
if (response != null) {
|
|
||||||
response.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return str.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void storeTranscript(FeedMedia media, String transcript) {
|
|
||||||
File transcriptFile = new File(media.getTranscriptFileUrl());
|
|
||||||
FileOutputStream ostream = null;
|
|
||||||
try {
|
|
||||||
if (!transcriptFile.exists() && transcriptFile.createNewFile()) {
|
|
||||||
ostream = new FileOutputStream(transcriptFile);
|
|
||||||
ostream.write(transcript.getBytes(Charset.forName("UTF-8")));
|
|
||||||
ostream.close();
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
} finally {
|
|
||||||
IOUtils.closeQuietly(ostream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="?attr/action_icon_color"
|
||||||
|
android:pathData="M240,640L560,640L560,560L240,560L240,640ZM640,640L720,640L720,560L640,560L640,640ZM240,480L320,480L320,400L240,400L240,480ZM400,480L720,480L720,400L400,400L400,480ZM160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800ZM160,720L800,720Q800,720 800,720Q800,720 800,720L800,240Q800,240 800,240Q800,240 800,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720ZM160,720Q160,720 160,720Q160,720 160,720L160,240Q160,240 160,240Q160,240 160,240L160,240Q160,240 160,240Q160,240 160,240L160,720Q160,720 160,720Q160,720 160,720L160,720Z"/>
|
||||||
|
</vector>
|
|
@ -258,6 +258,10 @@
|
||||||
<item quantity="other">%d episodes removed from inbox.</item>
|
<item quantity="other">%d episodes removed from inbox.</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="add_to_favorite_label">Add to favorites</string>
|
<string name="add_to_favorite_label">Add to favorites</string>
|
||||||
|
<string name="show_transcript">Show transcript</string>
|
||||||
|
<string name="transcript">Transcript</string>
|
||||||
|
<string name="transcript_follow">Follow audio</string>
|
||||||
|
<string name="no_transcript_label">No transcript</string>
|
||||||
<string name="remove_from_favorite_label">Remove from favorites</string>
|
<string name="remove_from_favorite_label">Remove from favorites</string>
|
||||||
<string name="visit_website_label">Visit website</string>
|
<string name="visit_website_label">Visit website</string>
|
||||||
<string name="skip_episode_label">Skip episode</string>
|
<string name="skip_episode_label">Skip episode</string>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
plugins {
|
||||||
|
id("com.android.library")
|
||||||
|
}
|
||||||
|
apply from: "../../common.gradle"
|
||||||
|
apply from: "../../playFlavor.gradle"
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace "de.danoeh.antennapod.ui.transcript"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation project(':model')
|
||||||
|
implementation project(':net:common')
|
||||||
|
implementation project(':parser:media')
|
||||||
|
implementation project(':parser:transcript')
|
||||||
|
|
||||||
|
implementation "commons-io:commons-io:$commonsioVersion"
|
||||||
|
implementation "org.apache.commons:commons-lang3:$commonslangVersion"
|
||||||
|
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package de.danoeh.antennapod.ui.transcript;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import org.apache.commons.io.FileUtils;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InterruptedIOException;
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import de.danoeh.antennapod.model.feed.FeedMedia;
|
||||||
|
import de.danoeh.antennapod.net.common.AntennapodHttpClient;
|
||||||
|
import de.danoeh.antennapod.model.feed.Transcript;
|
||||||
|
import de.danoeh.antennapod.parser.transcript.TranscriptParser;
|
||||||
|
import okhttp3.CacheControl;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.Response;
|
||||||
|
import org.apache.commons.io.IOUtils;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
public class TranscriptUtils {
|
||||||
|
private static final String TAG = "Transcript";
|
||||||
|
|
||||||
|
public static String loadTranscriptFromUrl(String url, boolean forceRefresh) throws InterruptedIOException {
|
||||||
|
if (forceRefresh) {
|
||||||
|
return loadTranscriptFromUrl(url, CacheControl.FORCE_NETWORK);
|
||||||
|
}
|
||||||
|
String str = loadTranscriptFromUrl(url, CacheControl.FORCE_CACHE);
|
||||||
|
if (str == null || str.length() <= 1) {
|
||||||
|
// Some publishers use one dummy transcript before actual transcript are available
|
||||||
|
return loadTranscriptFromUrl(url, CacheControl.FORCE_NETWORK);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String loadTranscriptFromUrl(String url, CacheControl cacheControl) throws InterruptedIOException {
|
||||||
|
StringBuilder str = new StringBuilder();
|
||||||
|
Response response = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
Log.d(TAG, "Downloading transcript URL " + url);
|
||||||
|
Request request = new Request.Builder().url(url).cacheControl(cacheControl).build();
|
||||||
|
response = AntennapodHttpClient.getHttpClient().newCall(request).execute();
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
Log.d(TAG, "Done Downloading transcript URL " + url);
|
||||||
|
str.append(response.body().string());
|
||||||
|
} else {
|
||||||
|
Log.d(TAG, "Error Downloading transcript URL " + url + ": " + response.message());
|
||||||
|
}
|
||||||
|
} catch (InterruptedIOException e) {
|
||||||
|
Log.d(TAG, "InterruptedIOException while downloading transcript URL " + url);
|
||||||
|
throw e;
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
if (response != null) {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Transcript loadTranscript(FeedMedia media, Boolean forceRefresh) throws InterruptedIOException {
|
||||||
|
String transcriptType = media.getItem().getTranscriptType();
|
||||||
|
|
||||||
|
if (!forceRefresh && media.getItem().getTranscript() != null) {
|
||||||
|
return media.getTranscript();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!forceRefresh && media.getTranscriptFileUrl() != null) {
|
||||||
|
File transcriptFile = new File(media.getTranscriptFileUrl());
|
||||||
|
try {
|
||||||
|
if (transcriptFile.exists()) {
|
||||||
|
String t = FileUtils.readFileToString(transcriptFile, (String) null);
|
||||||
|
if (StringUtils.isNotEmpty(t)) {
|
||||||
|
media.setTranscript(TranscriptParser.parse(t, transcriptType));
|
||||||
|
return media.getTranscript();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String transcriptUrl = media.getItem().getTranscriptUrl();
|
||||||
|
String t = TranscriptUtils.loadTranscriptFromUrl(transcriptUrl, forceRefresh);
|
||||||
|
if (StringUtils.isNotEmpty(t)) {
|
||||||
|
return TranscriptParser.parse(t, transcriptType);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void storeTranscript(FeedMedia media, String transcript) {
|
||||||
|
File transcriptFile = new File(media.getTranscriptFileUrl());
|
||||||
|
FileOutputStream ostream = null;
|
||||||
|
try {
|
||||||
|
if (transcriptFile.exists() && !transcriptFile.delete()) {
|
||||||
|
Log.e(TAG, "Failed to delete existing transcript file " + transcriptFile.getAbsolutePath());
|
||||||
|
}
|
||||||
|
if (transcriptFile.createNewFile()) {
|
||||||
|
ostream = new FileOutputStream(transcriptFile);
|
||||||
|
ostream.write(transcript.getBytes(Charset.forName("UTF-8")));
|
||||||
|
ostream.close();
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
IOUtils.closeQuietly(ostream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue