Merge remote-tracking branch 'upstream/develop' into player-screen-update
@ -58,7 +58,8 @@ class DBTestUtils {
|
||||
List<Chapter> chapters = new ArrayList<>();
|
||||
item.setChapters(chapters);
|
||||
for (int k = 0; k < numChapters; k++) {
|
||||
chapters.add(new SimpleChapter(k, "item " + j + " chapter " + k, item, "http://example.com"));
|
||||
chapters.add(new SimpleChapter(k, "item " + j + " chapter " + k,
|
||||
"http://example.com", "http://example.com/image.png"));
|
||||
}
|
||||
}
|
||||
f.getItems().add(item);
|
||||
|
@ -1,6 +1,7 @@
|
||||
package de.danoeh.antennapod.adapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@ -9,10 +10,15 @@ import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.feed.Chapter;
|
||||
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
|
||||
import de.danoeh.antennapod.core.glide.ApGlideSettings;
|
||||
import de.danoeh.antennapod.core.util.ChapterUtils;
|
||||
import de.danoeh.antennapod.core.util.Converter;
|
||||
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
|
||||
import de.danoeh.antennapod.core.util.IntentUtils;
|
||||
import de.danoeh.antennapod.core.util.ThemeUtils;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
@ -23,6 +29,7 @@ public class ChaptersListAdapter extends ArrayAdapter<Chapter> {
|
||||
private Playable media;
|
||||
private final Callback callback;
|
||||
private int currentChapterIndex = -1;
|
||||
private boolean hasImages = false;
|
||||
|
||||
public ChaptersListAdapter(Context context, int textViewResourceId, Callback callback) {
|
||||
super(context, textViewResourceId);
|
||||
@ -31,6 +38,15 @@ public class ChaptersListAdapter extends ArrayAdapter<Chapter> {
|
||||
|
||||
public void setMedia(Playable media) {
|
||||
this.media = media;
|
||||
hasImages = false;
|
||||
if (media.getChapters() != null) {
|
||||
for (Chapter chapter : media.getChapters()) {
|
||||
if (!ignoreChapter(chapter) && !TextUtils.isEmpty(chapter.getImageUrl())) {
|
||||
hasImages = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@ -51,6 +67,7 @@ public class ChaptersListAdapter extends ArrayAdapter<Chapter> {
|
||||
holder.title = convertView.findViewById(R.id.txtvTitle);
|
||||
holder.start = convertView.findViewById(R.id.txtvStart);
|
||||
holder.link = convertView.findViewById(R.id.txtvLink);
|
||||
holder.image = convertView.findViewById(R.id.imgvCover);
|
||||
holder.duration = convertView.findViewById(R.id.txtvDuration);
|
||||
holder.secondaryActionButton = convertView.findViewById(R.id.secondaryActionButton);
|
||||
holder.secondaryActionIcon = convertView.findViewById(R.id.secondaryActionIcon);
|
||||
@ -94,6 +111,23 @@ public class ChaptersListAdapter extends ArrayAdapter<Chapter> {
|
||||
holder.view.setBackgroundColor(ContextCompat.getColor(getContext(), android.R.color.transparent));
|
||||
}
|
||||
|
||||
if (hasImages) {
|
||||
holder.image.setVisibility(View.VISIBLE);
|
||||
if (TextUtils.isEmpty(sc.getImageUrl())) {
|
||||
Glide.with(getContext()).clear(holder.image);
|
||||
} else {
|
||||
Glide.with(getContext())
|
||||
.load(EmbeddedChapterImage.getModelFor(media, position))
|
||||
.apply(new RequestOptions()
|
||||
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
|
||||
.dontAnimate()
|
||||
.fitCenter())
|
||||
.into(holder.image);
|
||||
}
|
||||
} else {
|
||||
holder.image.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@ -103,6 +137,7 @@ public class ChaptersListAdapter extends ArrayAdapter<Chapter> {
|
||||
TextView start;
|
||||
TextView link;
|
||||
TextView duration;
|
||||
ImageView image;
|
||||
View secondaryActionButton;
|
||||
ImageView secondaryActionIcon;
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.ListView;
|
||||
|
||||
import de.danoeh.antennapod.core.util.ChapterUtils;
|
||||
import java.util.List;
|
||||
import java.util.ListIterator;
|
||||
|
||||
@ -104,18 +105,10 @@ public class ChaptersFragment extends ListFragment {
|
||||
}
|
||||
|
||||
private int getCurrentChapter(Playable media) {
|
||||
if (media == null || media.getChapters() == null || media.getChapters().size() == 0 || controller == null) {
|
||||
if (controller == null) {
|
||||
return -1;
|
||||
}
|
||||
int currentPosition = controller.getPosition();
|
||||
|
||||
List<Chapter> chapters = media.getChapters();
|
||||
for (int i = 0; i < chapters.size(); i++) {
|
||||
if (chapters.get(i).getStart() > currentPosition) {
|
||||
return i - 1;
|
||||
}
|
||||
}
|
||||
return chapters.size() - 1;
|
||||
return ChapterUtils.getCurrentChapterIndex(media, controller.getPosition());
|
||||
}
|
||||
|
||||
private void loadMediaInfo() {
|
||||
|
@ -1,29 +1,36 @@
|
||||
package de.danoeh.antennapod.fragment;
|
||||
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter;
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.core.glide.ApGlideSettings;
|
||||
import de.danoeh.antennapod.core.event.PlaybackPositionEvent;
|
||||
import de.danoeh.antennapod.core.feed.util.ImageResourceUtils;
|
||||
import de.danoeh.antennapod.core.glide.ApGlideSettings;
|
||||
import de.danoeh.antennapod.core.util.ChapterUtils;
|
||||
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
import de.danoeh.antennapod.core.util.playback.PlaybackController;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Displays the cover and the title of a FeedItem.
|
||||
@ -38,6 +45,8 @@ public class CoverFragment extends Fragment {
|
||||
private ImageView imgvCover;
|
||||
private PlaybackController controller;
|
||||
private Disposable disposable;
|
||||
private int displayedChapterIndex = -2;
|
||||
private Playable media;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
@ -55,31 +64,26 @@ public class CoverFragment extends Fragment {
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
}
|
||||
disposable = Maybe.create(emitter -> {
|
||||
Playable media = controller.getMedia();
|
||||
if (media != null) {
|
||||
emitter.onSuccess(media);
|
||||
} else {
|
||||
emitter.onComplete();
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(media -> displayMediaInfo((Playable) media),
|
||||
error -> Log.e(TAG, Log.getStackTraceString(error)));
|
||||
disposable = Maybe.<Playable>create(emitter -> {
|
||||
Playable media = controller.getMedia();
|
||||
if (media != null) {
|
||||
emitter.onSuccess(media);
|
||||
} else {
|
||||
emitter.onComplete();
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(media -> {
|
||||
this.media = media;
|
||||
displayMediaInfo(media);
|
||||
}, error -> Log.e(TAG, Log.getStackTraceString(error)));
|
||||
}
|
||||
|
||||
private void displayMediaInfo(@NonNull Playable media) {
|
||||
txtvPodcastTitle.setText(media.getFeedTitle());
|
||||
txtvEpisodeTitle.setText(media.getEpisodeTitle());
|
||||
Glide.with(this)
|
||||
.load(ImageResourceUtils.getImageLocation(media))
|
||||
.apply(new RequestOptions()
|
||||
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
|
||||
.dontAnimate()
|
||||
.transforms(new FitCenter(),
|
||||
new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))))
|
||||
.into(imgvCover);
|
||||
displayCoverImage(media.getPosition());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -102,6 +106,7 @@ public class CoverFragment extends Fragment {
|
||||
};
|
||||
controller.init();
|
||||
loadMediaInfo();
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -109,6 +114,44 @@ public class CoverFragment extends Fragment {
|
||||
super.onStop();
|
||||
controller.release();
|
||||
controller = null;
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEventMainThread(PlaybackPositionEvent event) {
|
||||
if (controller == null) {
|
||||
return;
|
||||
}
|
||||
displayCoverImage(event.getPosition());
|
||||
}
|
||||
|
||||
private void displayCoverImage(int position) {
|
||||
int chapter = ChapterUtils.getCurrentChapterIndex(media, position);
|
||||
if (chapter != displayedChapterIndex) {
|
||||
displayedChapterIndex = chapter;
|
||||
|
||||
RequestBuilder<Drawable> cover = Glide.with(this)
|
||||
.load(ImageResourceUtils.getImageLocation(media))
|
||||
.apply(new RequestOptions()
|
||||
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
|
||||
.dontAnimate()
|
||||
.transforms(new FitCenter(),
|
||||
new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))));
|
||||
if (chapter == -1 || TextUtils.isEmpty(media.getChapters().get(chapter).getImageUrl())) {
|
||||
cover.into(imgvCover);
|
||||
} else {
|
||||
Glide.with(this)
|
||||
.load(EmbeddedChapterImage.getModelFor(media, chapter))
|
||||
.apply(new RequestOptions()
|
||||
.diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY)
|
||||
.dontAnimate()
|
||||
.transforms(new FitCenter(),
|
||||
new RoundedCorners((int) (16 * getResources().getDisplayMetrics().density))))
|
||||
.thumbnail(cover)
|
||||
.error(cover)
|
||||
.into(imgvCover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/fragmentLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/external_player_height"
|
||||
@ -44,7 +44,9 @@
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/pause_label"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:src="?attr/av_play_big"
|
||||
app:srcCompat="?attr/av_play"
|
||||
android:scaleType="fitCenter"
|
||||
android:padding="8dp"
|
||||
tools:src="@drawable/ic_play_arrow_white_36dp"/>
|
||||
|
||||
<TextView
|
||||
|
@ -119,9 +119,9 @@
|
||||
android:layout_centerHorizontal="true"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pause_label"
|
||||
android:src="?attr/av_play"
|
||||
app:srcCompat="?attr/av_play"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/ic_play_arrow_white_24dp"
|
||||
tools:src="@drawable/ic_av_play_white_24dp"
|
||||
tools:background="@android:color/holo_green_dark" />
|
||||
|
||||
<ImageButton
|
||||
@ -134,9 +134,9 @@
|
||||
android:layout_marginStart="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/rewind_label"
|
||||
android:src="?attr/av_rew_big"
|
||||
app:srcCompat="?attr/av_rewind"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/ic_fast_rewind_white_36dp"
|
||||
tools:src="@drawable/ic_av_fast_rewind_white_48dp"
|
||||
tools:background="@android:color/holo_blue_dark" />
|
||||
|
||||
<TextView
|
||||
@ -163,7 +163,7 @@
|
||||
android:layout_toStartOf="@id/butRev"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/set_playback_speed_label"
|
||||
android:src="?attr/av_speed"
|
||||
app:srcCompat="?attr/av_speed"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/ic_playback_speed_white_48dp"
|
||||
tools:visibility="gone"
|
||||
@ -210,9 +210,9 @@
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/fast_forward_label"
|
||||
android:src="?attr/av_ff_big"
|
||||
app:srcCompat="?attr/av_fast_forward"
|
||||
android:scaleType="fitCenter"
|
||||
tools:src="@drawable/ic_fast_forward_white_36dp"
|
||||
tools:src="@drawable/ic_av_fast_forward_white_48dp"
|
||||
tools:background="@android:color/holo_blue_dark" />
|
||||
|
||||
<TextView
|
||||
@ -239,9 +239,9 @@
|
||||
android:layout_toEndOf="@id/butFF"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:scaleType="fitCenter"
|
||||
android:src="?attr/av_skip_big"
|
||||
app:srcCompat="?attr/av_skip"
|
||||
android:contentDescription="@string/skip_episode_label"
|
||||
tools:src="@drawable/ic_skip_white_36dp"
|
||||
tools:src="@drawable/ic_av_skip_white_48dp"
|
||||
tools:background="@android:color/holo_green_dark" />
|
||||
</RelativeLayout>
|
||||
|
||||
|
@ -10,6 +10,15 @@
|
||||
android:baselineAligned="false"
|
||||
android:descendantFocusability="blocksDescendants">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imgvCover"
|
||||
android:layout_width="@dimen/thumbnail_length_queue_item"
|
||||
android:layout_height="@dimen/thumbnail_length_queue_item"
|
||||
android:contentDescription="@string/cover_label"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginStart="16dp"
|
||||
tools:src="@tools:sample/avatars"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -27,6 +27,8 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:layoutDirection="ltr"
|
||||
android:background="@android:color/transparent"
|
||||
android:padding="16dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
@ -34,7 +36,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/rewind_label"
|
||||
app:srcCompat="@drawable/ic_av_fast_rewind_white_80dp" />
|
||||
|
||||
@ -43,7 +45,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/pause_label"
|
||||
app:srcCompat="@drawable/ic_av_pause_white_80dp" />
|
||||
|
||||
@ -52,7 +54,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/fast_forward_label"
|
||||
app:srcCompat="@drawable/ic_av_fast_forward_white_80dp" />
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
||||
android:key="prefScreenPlayback"
|
||||
android:title="@string/playback_pref"
|
||||
android:summary="@string/playback_pref_sum"
|
||||
android:icon="?attr/av_play" />
|
||||
android:icon="?attr/ic_settings_playback" />
|
||||
|
||||
<Preference
|
||||
android:key="prefScreenNetwork"
|
||||
|
@ -6,81 +6,92 @@ import de.danoeh.antennapod.core.storage.PodDBAdapter;
|
||||
|
||||
public abstract class Chapter extends FeedComponent {
|
||||
|
||||
/** Defines starting point in milliseconds. */
|
||||
/** Defines starting point in milliseconds. */
|
||||
long start;
|
||||
String title;
|
||||
String link;
|
||||
String title;
|
||||
String link;
|
||||
String imageUrl;
|
||||
|
||||
Chapter() {
|
||||
}
|
||||
|
||||
Chapter(long start) {
|
||||
super();
|
||||
this.start = start;
|
||||
}
|
||||
Chapter() {
|
||||
}
|
||||
|
||||
Chapter(long start, String title, FeedItem item, String link) {
|
||||
super();
|
||||
this.start = start;
|
||||
this.title = title;
|
||||
this.link = link;
|
||||
}
|
||||
Chapter(long start) {
|
||||
super();
|
||||
this.start = start;
|
||||
}
|
||||
|
||||
public static Chapter fromCursor(Cursor cursor, FeedItem item) {
|
||||
int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
|
||||
int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
|
||||
int indexStart = cursor.getColumnIndex(PodDBAdapter.KEY_START);
|
||||
int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
|
||||
int indexChapterType = cursor.getColumnIndex(PodDBAdapter.KEY_CHAPTER_TYPE);
|
||||
Chapter(long start, String title, String link, String imageUrl) {
|
||||
super();
|
||||
this.start = start;
|
||||
this.title = title;
|
||||
this.link = link;
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
long id = cursor.getLong(indexId);
|
||||
String title = cursor.getString(indexTitle);
|
||||
long start = cursor.getLong(indexStart);
|
||||
String link = cursor.getString(indexLink);
|
||||
int chapterType = cursor.getInt(indexChapterType);
|
||||
public static Chapter fromCursor(Cursor cursor) {
|
||||
int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID);
|
||||
int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE);
|
||||
int indexStart = cursor.getColumnIndex(PodDBAdapter.KEY_START);
|
||||
int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK);
|
||||
int indexImage = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL);
|
||||
int indexChapterType = cursor.getColumnIndex(PodDBAdapter.KEY_CHAPTER_TYPE);
|
||||
|
||||
Chapter chapter = null;
|
||||
switch (chapterType) {
|
||||
case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
|
||||
chapter = new SimpleChapter(start, title, item, link);
|
||||
break;
|
||||
case ID3Chapter.CHAPTERTYPE_ID3CHAPTER:
|
||||
chapter = new ID3Chapter(start, title, item, link);
|
||||
break;
|
||||
case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER:
|
||||
chapter = new VorbisCommentChapter(start, title, item, link);
|
||||
break;
|
||||
}
|
||||
chapter.setId(id);
|
||||
return chapter;
|
||||
}
|
||||
long id = cursor.getLong(indexId);
|
||||
String title = cursor.getString(indexTitle);
|
||||
long start = cursor.getLong(indexStart);
|
||||
String link = cursor.getString(indexLink);
|
||||
String imageUrl = cursor.getString(indexImage);
|
||||
int chapterType = cursor.getInt(indexChapterType);
|
||||
|
||||
Chapter chapter = null;
|
||||
switch (chapterType) {
|
||||
case SimpleChapter.CHAPTERTYPE_SIMPLECHAPTER:
|
||||
chapter = new SimpleChapter(start, title, link, imageUrl);
|
||||
break;
|
||||
case ID3Chapter.CHAPTERTYPE_ID3CHAPTER:
|
||||
chapter = new ID3Chapter(start, title, link, imageUrl);
|
||||
break;
|
||||
case VorbisCommentChapter.CHAPTERTYPE_VORBISCOMMENT_CHAPTER:
|
||||
chapter = new VorbisCommentChapter(start, title, link, imageUrl);
|
||||
break;
|
||||
}
|
||||
chapter.setId(id);
|
||||
return chapter;
|
||||
}
|
||||
|
||||
public abstract int getChapterType();
|
||||
public abstract int getChapterType();
|
||||
|
||||
public long getStart() {
|
||||
return start;
|
||||
}
|
||||
public long getStart() {
|
||||
return start;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
public String getLink() {
|
||||
return link;
|
||||
}
|
||||
|
||||
public void setStart(long start) {
|
||||
this.start = start;
|
||||
}
|
||||
public void setStart(long start) {
|
||||
this.start = start;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
public void setLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHumanReadableIdentifier() {
|
||||
|
@ -1,36 +1,36 @@
|
||||
package de.danoeh.antennapod.core.feed;
|
||||
|
||||
public class ID3Chapter extends Chapter {
|
||||
public static final int CHAPTERTYPE_ID3CHAPTER = 2;
|
||||
public static final int CHAPTERTYPE_ID3CHAPTER = 2;
|
||||
|
||||
/**
|
||||
* Identifies the chapter in its ID3 tag. This attribute does not have to be
|
||||
* store in the DB and is only used for parsing.
|
||||
*/
|
||||
private String id3ID;
|
||||
/**
|
||||
* Identifies the chapter in its ID3 tag. This attribute does not have to be
|
||||
* store in the DB and is only used for parsing.
|
||||
*/
|
||||
private String id3ID;
|
||||
|
||||
public ID3Chapter(String id3ID, long start) {
|
||||
super(start);
|
||||
this.id3ID = id3ID;
|
||||
}
|
||||
public ID3Chapter(String id3ID, long start) {
|
||||
super(start);
|
||||
this.id3ID = id3ID;
|
||||
}
|
||||
|
||||
public ID3Chapter(long start, String title, FeedItem item, String link) {
|
||||
super(start, title, item, link);
|
||||
}
|
||||
public ID3Chapter(long start, String title, String link, String imageUrl) {
|
||||
super(start, title, link, imageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ID3Chapter [id3ID=" + id3ID + ", title=" + title + ", start="
|
||||
+ start + ", url=" + link + "]";
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "ID3Chapter [id3ID=" + id3ID + ", title=" + title + ", start="
|
||||
+ start + ", url=" + link + "]";
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChapterType() {
|
||||
return CHAPTERTYPE_ID3CHAPTER;
|
||||
}
|
||||
@Override
|
||||
public int getChapterType() {
|
||||
return CHAPTERTYPE_ID3CHAPTER;
|
||||
}
|
||||
|
||||
public String getId3ID() {
|
||||
return id3ID;
|
||||
}
|
||||
public String getId3ID() {
|
||||
return id3ID;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,25 +1,14 @@
|
||||
package de.danoeh.antennapod.core.feed;
|
||||
|
||||
public class SimpleChapter extends Chapter {
|
||||
public static final int CHAPTERTYPE_SIMPLECHAPTER = 0;
|
||||
|
||||
public SimpleChapter(long start, String title, FeedItem item, String link) {
|
||||
super(start, title, item, link);
|
||||
}
|
||||
public static final int CHAPTERTYPE_SIMPLECHAPTER = 0;
|
||||
|
||||
@Override
|
||||
public int getChapterType() {
|
||||
return CHAPTERTYPE_SIMPLECHAPTER;
|
||||
}
|
||||
public SimpleChapter(long start, String title, String link, String imageUrl) {
|
||||
super(start, title, link, imageUrl);
|
||||
}
|
||||
|
||||
public void updateFromOther(SimpleChapter other) {
|
||||
super.updateFromOther(other);
|
||||
start = other.start;
|
||||
if (other.title != null) {
|
||||
title = other.title;
|
||||
}
|
||||
if (other.link != null) {
|
||||
link = other.link;
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public int getChapterType() {
|
||||
return CHAPTERTYPE_SIMPLECHAPTER;
|
||||
}
|
||||
}
|
||||
|
@ -5,105 +5,84 @@ import java.util.concurrent.TimeUnit;
|
||||
import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException;
|
||||
|
||||
public class VorbisCommentChapter extends Chapter {
|
||||
public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3;
|
||||
public static final int CHAPTERTYPE_VORBISCOMMENT_CHAPTER = 3;
|
||||
|
||||
private static final int CHAPTERXXX_LENGTH = "chapterxxx".length();
|
||||
private static final int CHAPTERXXX_LENGTH = "chapterxxx".length();
|
||||
|
||||
private int vorbisCommentId;
|
||||
private int vorbisCommentId;
|
||||
|
||||
public VorbisCommentChapter(int vorbisCommentId) {
|
||||
this.vorbisCommentId = vorbisCommentId;
|
||||
}
|
||||
public VorbisCommentChapter(int vorbisCommentId) {
|
||||
this.vorbisCommentId = vorbisCommentId;
|
||||
}
|
||||
|
||||
public VorbisCommentChapter(long start, String title, FeedItem item,
|
||||
String link) {
|
||||
super(start, title, item, link);
|
||||
}
|
||||
public VorbisCommentChapter(long start, String title, String link, String imageUrl) {
|
||||
super(start, title, link, imageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VorbisCommentChapter [id=" + id + ", title=" + title
|
||||
+ ", link=" + link + ", start=" + start + "]";
|
||||
}
|
||||
@Override
|
||||
public String toString() {
|
||||
return "VorbisCommentChapter [id=" + id + ", title=" + title
|
||||
+ ", link=" + link + ", start=" + start + "]";
|
||||
}
|
||||
|
||||
public static long getStartTimeFromValue(String value)
|
||||
throws VorbisCommentReaderException {
|
||||
String[] parts = value.split(":");
|
||||
if (parts.length >= 3) {
|
||||
try {
|
||||
long hours = TimeUnit.MILLISECONDS.convert(
|
||||
Long.parseLong(parts[0]), TimeUnit.HOURS);
|
||||
long minutes = TimeUnit.MILLISECONDS.convert(
|
||||
Long.parseLong(parts[1]), TimeUnit.MINUTES);
|
||||
if (parts[2].contains("-->")) {
|
||||
parts[2] = parts[2].substring(0, parts[2].indexOf("-->"));
|
||||
}
|
||||
long seconds = TimeUnit.MILLISECONDS.convert(
|
||||
((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS);
|
||||
return hours + minutes + seconds;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new VorbisCommentReaderException(e);
|
||||
}
|
||||
} else {
|
||||
throw new VorbisCommentReaderException("Invalid time string");
|
||||
}
|
||||
}
|
||||
public static long getStartTimeFromValue(String value)
|
||||
throws VorbisCommentReaderException {
|
||||
String[] parts = value.split(":");
|
||||
if (parts.length >= 3) {
|
||||
try {
|
||||
long hours = TimeUnit.MILLISECONDS.convert(
|
||||
Long.parseLong(parts[0]), TimeUnit.HOURS);
|
||||
long minutes = TimeUnit.MILLISECONDS.convert(
|
||||
Long.parseLong(parts[1]), TimeUnit.MINUTES);
|
||||
if (parts[2].contains("-->")) {
|
||||
parts[2] = parts[2].substring(0, parts[2].indexOf("-->"));
|
||||
}
|
||||
long seconds = TimeUnit.MILLISECONDS.convert(
|
||||
((long) Float.parseFloat(parts[2])), TimeUnit.SECONDS);
|
||||
return hours + minutes + seconds;
|
||||
} catch (NumberFormatException e) {
|
||||
throw new VorbisCommentReaderException(e);
|
||||
}
|
||||
} else {
|
||||
throw new VorbisCommentReaderException("Invalid time string");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the id of a vorbiscomment chapter from a string like CHAPTERxxx*
|
||||
*
|
||||
* @return the id of the chapter key or -1 if the id couldn't be read.
|
||||
* @throws VorbisCommentReaderException
|
||||
* */
|
||||
public static int getIDFromKey(String key)
|
||||
throws VorbisCommentReaderException {
|
||||
if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx
|
||||
try {
|
||||
String strId = key.substring(8, 10);
|
||||
return Integer.parseInt(strId);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new VorbisCommentReaderException(e);
|
||||
}
|
||||
}
|
||||
throw new VorbisCommentReaderException("key is too short (" + key + ")");
|
||||
}
|
||||
/**
|
||||
* Return the id of a vorbiscomment chapter from a string like CHAPTERxxx*
|
||||
*
|
||||
* @return the id of the chapter key or -1 if the id couldn't be read.
|
||||
* @throws VorbisCommentReaderException
|
||||
* */
|
||||
public static int getIDFromKey(String key) throws VorbisCommentReaderException {
|
||||
if (key.length() >= CHAPTERXXX_LENGTH) { // >= CHAPTERxxx
|
||||
try {
|
||||
String strId = key.substring(8, 10);
|
||||
return Integer.parseInt(strId);
|
||||
} catch (NumberFormatException e) {
|
||||
throw new VorbisCommentReaderException(e);
|
||||
}
|
||||
}
|
||||
throw new VorbisCommentReaderException("key is too short (" + key + ")");
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the string that comes after 'CHAPTERxxx', for example 'name' or
|
||||
* 'url'.
|
||||
*/
|
||||
public static String getAttributeTypeFromKey(String key) {
|
||||
if (key.length() > CHAPTERXXX_LENGTH) {
|
||||
return key.substring(CHAPTERXXX_LENGTH, key.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Get the string that comes after 'CHAPTERxxx', for example 'name' or
|
||||
* 'url'.
|
||||
*/
|
||||
public static String getAttributeTypeFromKey(String key) {
|
||||
if (key.length() > CHAPTERXXX_LENGTH) {
|
||||
return key.substring(CHAPTERXXX_LENGTH);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChapterType() {
|
||||
return CHAPTERTYPE_VORBISCOMMENT_CHAPTER;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public void setLink(String link) {
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
public void setStart(long start) {
|
||||
this.start = start;
|
||||
}
|
||||
|
||||
public int getVorbisCommentId() {
|
||||
return vorbisCommentId;
|
||||
}
|
||||
|
||||
public void setVorbisCommentId(int vorbisCommentId) {
|
||||
this.vorbisCommentId = vorbisCommentId;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int getChapterType() {
|
||||
return CHAPTERTYPE_VORBISCOMMENT_CHAPTER;
|
||||
}
|
||||
|
||||
public int getVorbisCommentId() {
|
||||
return vorbisCommentId;
|
||||
}
|
||||
}
|
||||
|
@ -11,10 +11,12 @@ import com.bumptech.glide.load.DecodeFormat;
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
|
||||
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
|
||||
import java.io.InputStream;
|
||||
|
||||
import com.bumptech.glide.request.RequestOptions;
|
||||
import de.danoeh.antennapod.core.preferences.UserPreferences;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
/**
|
||||
* {@see com.bumptech.glide.integration.okhttp.OkHttpGlideModule}
|
||||
@ -32,5 +34,6 @@ public class ApGlideModule extends AppGlideModule {
|
||||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||
registry.replace(String.class, InputStream.class, new ApOkHttpUrlLoader.Factory());
|
||||
registry.append(EmbeddedChapterImage.class, ByteBuffer.class, new ChapterImageModelLoader.Factory());
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,111 @@
|
||||
package de.danoeh.antennapod.core.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory;
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import de.danoeh.antennapod.core.ClientConfig;
|
||||
import de.danoeh.antennapod.core.feed.Chapter;
|
||||
import de.danoeh.antennapod.core.service.download.AntennapodHttpClient;
|
||||
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.nio.ByteBuffer;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
public final class ChapterImageModelLoader implements ModelLoader<EmbeddedChapterImage, ByteBuffer> {
|
||||
|
||||
public static class Factory implements ModelLoaderFactory<EmbeddedChapterImage, ByteBuffer> {
|
||||
@Override
|
||||
public ModelLoader<EmbeddedChapterImage, ByteBuffer> build(MultiModelLoaderFactory unused) {
|
||||
return new ChapterImageModelLoader();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void teardown() {
|
||||
// Do nothing.
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public LoadData<ByteBuffer> buildLoadData(EmbeddedChapterImage model, int width, int height, Options options) {
|
||||
return new LoadData<>(new ObjectKey(model), new EmbeddedImageFetcher(model));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(EmbeddedChapterImage model) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static class EmbeddedImageFetcher implements DataFetcher<ByteBuffer> {
|
||||
private final EmbeddedChapterImage image;
|
||||
|
||||
public EmbeddedImageFetcher(EmbeddedChapterImage image) {
|
||||
this.image = image;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super ByteBuffer> callback) {
|
||||
|
||||
BufferedInputStream stream = null;
|
||||
try {
|
||||
if (image.getMedia().localFileAvailable()) {
|
||||
File localFile = new File(image.getMedia().getLocalMediaUrl());
|
||||
stream = new BufferedInputStream(new FileInputStream(localFile));
|
||||
stream.skip(image.getPosition());
|
||||
byte[] imageContent = new byte[image.getLength()];
|
||||
stream.read(imageContent, 0, image.getLength());
|
||||
callback.onDataReady(ByteBuffer.wrap(imageContent));
|
||||
} else {
|
||||
Request.Builder httpReq = new Request.Builder();
|
||||
httpReq.header("User-Agent", ClientConfig.USER_AGENT);
|
||||
// Skipping would download the whole file
|
||||
httpReq.header("Range", "bytes=" + image.getPosition()
|
||||
+ "-" + (image.getPosition() + image.getLength()));
|
||||
httpReq.url(image.getMedia().getStreamUrl());
|
||||
Response response = AntennapodHttpClient.getHttpClient().newCall(httpReq.build()).execute();
|
||||
if (!response.isSuccessful() || response.body() == null) {
|
||||
throw new IOException("Invalid response: " + response.code() + " " + response.message());
|
||||
}
|
||||
callback.onDataReady(ByteBuffer.wrap(response.body().bytes()));
|
||||
}
|
||||
} catch (IOException e) {
|
||||
callback.onLoadFailed(e);
|
||||
} finally {
|
||||
IOUtils.closeQuietly(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override public void cleanup() {
|
||||
// nothing to clean up
|
||||
}
|
||||
@Override public void cancel() {
|
||||
// cannot cancel
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Class<ByteBuffer> getDataClass() {
|
||||
return ByteBuffer.class;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DataSource getDataSource() {
|
||||
return DataSource.LOCAL;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,19 +14,15 @@ public class FastBlurTransformation extends BitmapTransformation {
|
||||
|
||||
private static final String TAG = FastBlurTransformation.class.getSimpleName();
|
||||
|
||||
private static final int STACK_BLUR_RADIUS = 1;
|
||||
private static final int BLUR_IMAGE_WIDTH = 150;
|
||||
private static final int STACK_BLUR_RADIUS = 5;
|
||||
|
||||
public FastBlurTransformation() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Bitmap transform(BitmapPool pool, Bitmap source,
|
||||
int outWidth, int outHeight) {
|
||||
int targetWidth = BLUR_IMAGE_WIDTH;
|
||||
int targetHeight = (int) (1.0 * outHeight * targetWidth / outWidth);
|
||||
Bitmap resized = ThumbnailUtils.extractThumbnail(source, targetWidth, targetHeight);
|
||||
protected Bitmap transform(BitmapPool pool, Bitmap source, int outWidth, int outHeight) {
|
||||
Bitmap resized = ThumbnailUtils.extractThumbnail(source, outWidth / 3, outHeight / 3);
|
||||
Bitmap result = fastBlur(resized, STACK_BLUR_RADIUS);
|
||||
if (result == null) {
|
||||
Log.w(TAG, "result was null");
|
||||
@ -80,9 +76,9 @@ public class FastBlurTransformation extends BitmapTransformation {
|
||||
int wh = w * h;
|
||||
int div = radius + radius + 1;
|
||||
|
||||
int r[] = new int[wh];
|
||||
int g[] = new int[wh];
|
||||
int b[] = new int[wh];
|
||||
int[] r = new int[wh];
|
||||
int[] g = new int[wh];
|
||||
int[] b = new int[wh];
|
||||
int rsum;
|
||||
int gsum;
|
||||
int bsum;
|
||||
@ -93,11 +89,11 @@ public class FastBlurTransformation extends BitmapTransformation {
|
||||
int yp;
|
||||
int yi;
|
||||
int yw;
|
||||
int vmin[] = new int[Math.max(w, h)];
|
||||
int[] vmin = new int[Math.max(w, h)];
|
||||
|
||||
int divsum = (div + 1) >> 1;
|
||||
divsum *= divsum;
|
||||
int dv[] = new int[256 * divsum];
|
||||
int[] dv = new int[256 * divsum];
|
||||
for (i = 0; i < 256 * divsum; i++) {
|
||||
dv[i] = (i / divsum);
|
||||
}
|
||||
@ -225,8 +221,8 @@ public class FastBlurTransformation extends BitmapTransformation {
|
||||
yi = x;
|
||||
stackpointer = radius;
|
||||
for (y = 0; y < h; y++) {
|
||||
// Preserve alpha channel: ( 0xff000000 & pix[yi] )
|
||||
pix[yi] = (0xff000000 & pix[yi]) | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
|
||||
// Set alpha to 1
|
||||
pix[yi] = 0xff000000 | (dv[rsum] << 16) | (dv[gsum] << 8) | dv[bsum];
|
||||
|
||||
rsum -= routsum;
|
||||
gsum -= goutsum;
|
||||
|
@ -8,7 +8,6 @@ import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.IBinder;
|
||||
import androidx.annotation.NonNull;
|
||||
@ -158,10 +157,10 @@ public class PlayerWidgetJobService extends SafeJobIntentService {
|
||||
}
|
||||
|
||||
if (status == PlayerStatus.PLAYING) {
|
||||
views.setImageViewResource(R.id.butPlay, R.drawable.ic_pause_white_24dp);
|
||||
views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_pause_white_48dp);
|
||||
views.setContentDescription(R.id.butPlay, getString(R.string.pause_label));
|
||||
} else {
|
||||
views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp);
|
||||
views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp);
|
||||
views.setContentDescription(R.id.butPlay, getString(R.string.play_label));
|
||||
}
|
||||
views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent());
|
||||
@ -177,7 +176,7 @@ public class PlayerWidgetJobService extends SafeJobIntentService {
|
||||
views.setViewVisibility(R.id.txtvTitle, View.GONE);
|
||||
views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE);
|
||||
views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_foreground);
|
||||
views.setImageViewResource(R.id.butPlay, R.drawable.ic_play_arrow_white_24dp);
|
||||
views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp);
|
||||
}
|
||||
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||
|
@ -795,9 +795,7 @@ public final class DBReader {
|
||||
}
|
||||
|
||||
private static void loadChaptersOfFeedItem(PodDBAdapter adapter, FeedItem item) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = adapter.getSimpleChaptersOfFeedItemCursor(item);
|
||||
try (Cursor cursor = adapter.getSimpleChaptersOfFeedItemCursor(item)) {
|
||||
int chaptersCount = cursor.getCount();
|
||||
if (chaptersCount == 0) {
|
||||
item.setChapters(null);
|
||||
@ -805,14 +803,7 @@ public final class DBReader {
|
||||
}
|
||||
item.setChapters(new ArrayList<>(chaptersCount));
|
||||
while (cursor.moveToNext()) {
|
||||
Chapter chapter = Chapter.fromCursor(cursor, item);
|
||||
if (chapter != null) {
|
||||
item.getChapters().add(chapter);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
item.getChapters().add(Chapter.fromCursor(cursor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -95,7 +95,7 @@ class DBUpgrader {
|
||||
+ " ADD COLUMN " + PodDBAdapter.KEY_PASSWORD
|
||||
+ " TEXT");
|
||||
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEED_ITEMS
|
||||
+ " ADD COLUMN " + PodDBAdapter.KEY_IMAGE
|
||||
+ " ADD COLUMN image"
|
||||
+ " INTEGER");
|
||||
}
|
||||
if (oldVersion <= 12) {
|
||||
@ -280,13 +280,13 @@ class DBUpgrader {
|
||||
+ " SELECT " + PodDBAdapter.KEY_DOWNLOAD_URL
|
||||
+ " FROM " + PodDBAdapter.TABLE_NAME_FEED_IMAGES
|
||||
+ " WHERE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES + "." + PodDBAdapter.KEY_ID
|
||||
+ " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + "." + PodDBAdapter.KEY_IMAGE + ")");
|
||||
+ " = " + PodDBAdapter.TABLE_NAME_FEED_ITEMS + ".image)");
|
||||
|
||||
db.execSQL("UPDATE " + PodDBAdapter.TABLE_NAME_FEEDS + " SET " + PodDBAdapter.KEY_IMAGE_URL + " = ("
|
||||
+ " SELECT " + PodDBAdapter.KEY_DOWNLOAD_URL
|
||||
+ " FROM " + PodDBAdapter.TABLE_NAME_FEED_IMAGES
|
||||
+ " WHERE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES + "." + PodDBAdapter.KEY_ID
|
||||
+ " = " + PodDBAdapter.TABLE_NAME_FEEDS + "." + PodDBAdapter.KEY_IMAGE + ")");
|
||||
+ " = " + PodDBAdapter.TABLE_NAME_FEEDS + ".image)");
|
||||
|
||||
db.execSQL("DROP TABLE " + PodDBAdapter.TABLE_NAME_FEED_IMAGES);
|
||||
}
|
||||
@ -301,6 +301,8 @@ class DBUpgrader {
|
||||
if (oldVersion < 1090000) {
|
||||
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_FEEDS
|
||||
+ " ADD COLUMN " + PodDBAdapter.KEY_FEED_VOLUME_ADAPTION + " INTEGER DEFAULT 0");
|
||||
db.execSQL("ALTER TABLE " + PodDBAdapter.TABLE_NAME_SIMPLECHAPTERS
|
||||
+ " ADD COLUMN " + PodDBAdapter.KEY_IMAGE_URL + " TEXT DEFAULT NULL");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +69,6 @@ public class PodDBAdapter {
|
||||
public static final String KEY_POSITION = "position";
|
||||
public static final String KEY_SIZE = "filesize";
|
||||
public static final String KEY_MIME_TYPE = "mime_type";
|
||||
public static final String KEY_IMAGE = "image";
|
||||
public static final String KEY_IMAGE_URL = "image_url";
|
||||
public static final String KEY_FEED = "feed";
|
||||
public static final String KEY_MEDIA = "media";
|
||||
@ -183,7 +182,7 @@ public class PodDBAdapter {
|
||||
private static final String CREATE_TABLE_SIMPLECHAPTERS = "CREATE TABLE "
|
||||
+ TABLE_NAME_SIMPLECHAPTERS + " (" + TABLE_PRIMARY_KEY + KEY_TITLE
|
||||
+ " TEXT," + KEY_START + " INTEGER," + KEY_FEEDITEM + " INTEGER,"
|
||||
+ KEY_LINK + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)";
|
||||
+ KEY_LINK + " TEXT," + KEY_IMAGE_URL + " TEXT," + KEY_CHAPTER_TYPE + " INTEGER)";
|
||||
|
||||
// SQL Statements for creating indexes
|
||||
static final String CREATE_INDEX_FEEDITEMS_FEED = "CREATE INDEX "
|
||||
@ -674,6 +673,7 @@ public class PodDBAdapter {
|
||||
values.put(KEY_START, chapter.getStart());
|
||||
values.put(KEY_FEEDITEM, item.getId());
|
||||
values.put(KEY_LINK, chapter.getLink());
|
||||
values.put(KEY_IMAGE_URL, chapter.getImageUrl());
|
||||
values.put(KEY_CHAPTER_TYPE, chapter.getChapterType());
|
||||
if (chapter.getId() == 0) {
|
||||
chapter.setId(db.insert(TABLE_NAME_SIMPLECHAPTERS, null, values));
|
||||
|
@ -22,12 +22,12 @@ public class NSSimpleChapters extends Namespace {
|
||||
private static final String START = "start";
|
||||
private static final String TITLE = "title";
|
||||
private static final String HREF = "href";
|
||||
private static final String IMAGE = "image";
|
||||
|
||||
@Override
|
||||
public SyndElement handleElementStart(String localName, HandlerState state,
|
||||
Attributes attributes) {
|
||||
public SyndElement handleElementStart(String localName, HandlerState state, Attributes attributes) {
|
||||
FeedItem currentItem = state.getCurrentItem();
|
||||
if(currentItem != null) {
|
||||
if (currentItem != null) {
|
||||
if (localName.equals(CHAPTERS)) {
|
||||
currentItem.setChapters(new ArrayList<>());
|
||||
} else if (localName.equals(CHAPTER)) {
|
||||
@ -35,7 +35,8 @@ public class NSSimpleChapters extends Namespace {
|
||||
long start = DateUtils.parseTimeString(attributes.getValue(START));
|
||||
String title = attributes.getValue(TITLE);
|
||||
String link = attributes.getValue(HREF);
|
||||
SimpleChapter chapter = new SimpleChapter(start, title, currentItem, link);
|
||||
String imageUrl = attributes.getValue(IMAGE);
|
||||
SimpleChapter chapter = new SimpleChapter(start, title, link, imageUrl);
|
||||
currentItem.getChapters().add(chapter);
|
||||
} catch (NumberFormatException e) {
|
||||
Log.e(TAG, "Unable to read chapter", e);
|
||||
|
@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.zip.CheckedOutputStream;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
@ -23,6 +24,7 @@ import de.danoeh.antennapod.core.util.id3reader.ID3ReaderException;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentChapterReader;
|
||||
import de.danoeh.antennapod.core.util.vorbiscommentreader.VorbisCommentReaderException;
|
||||
import org.apache.commons.io.input.CountingInputStream;
|
||||
|
||||
/**
|
||||
* Utility class for getting chapter data from media files.
|
||||
@ -34,24 +36,17 @@ public class ChapterUtils {
|
||||
private ChapterUtils() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Chapter getCurrentChapter(Playable media) {
|
||||
if (media.getChapters() == null) {
|
||||
return null;
|
||||
public static int getCurrentChapterIndex(Playable media, int position) {
|
||||
if (media == null || media.getChapters() == null || media.getChapters().size() == 0) {
|
||||
return -1;
|
||||
}
|
||||
List<Chapter> chapters = media.getChapters();
|
||||
if (chapters == null) {
|
||||
return null;
|
||||
}
|
||||
Chapter current = chapters.get(0);
|
||||
for (Chapter sc : chapters) {
|
||||
if (sc.getStart() > media.getPosition()) {
|
||||
break;
|
||||
} else {
|
||||
current = sc;
|
||||
for (int i = 0; i < chapters.size(); i++) {
|
||||
if (chapters.get(i).getStart() > position) {
|
||||
return i - 1;
|
||||
}
|
||||
}
|
||||
return current;
|
||||
return chapters.size() - 1;
|
||||
}
|
||||
|
||||
public static void loadChaptersFromStreamUrl(Playable media) {
|
||||
@ -82,13 +77,12 @@ public class ChapterUtils {
|
||||
return;
|
||||
}
|
||||
Log.d(TAG, "Reading id3 chapters from item " + p.getEpisodeTitle());
|
||||
InputStream in = null;
|
||||
CountingInputStream in = null;
|
||||
try {
|
||||
URL url = new URL(p.getStreamUrl());
|
||||
|
||||
in = url.openStream();
|
||||
in = new CountingInputStream(url.openStream());
|
||||
List<Chapter> chapters = readChaptersFrom(in);
|
||||
if(!chapters.isEmpty()) {
|
||||
if (!chapters.isEmpty()) {
|
||||
p.setChapters(chapters);
|
||||
}
|
||||
Log.i(TAG, "Chapters loaded");
|
||||
@ -114,9 +108,9 @@ public class ChapterUtils {
|
||||
return;
|
||||
}
|
||||
|
||||
InputStream in = null;
|
||||
CountingInputStream in = null;
|
||||
try {
|
||||
in = new BufferedInputStream(new FileInputStream(source));
|
||||
in = new CountingInputStream(new BufferedInputStream(new FileInputStream(source)));
|
||||
List<Chapter> chapters = readChaptersFrom(in);
|
||||
if (!chapters.isEmpty()) {
|
||||
p.setChapters(chapters);
|
||||
@ -130,7 +124,7 @@ public class ChapterUtils {
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private static List<Chapter> readChaptersFrom(InputStream in) throws IOException, ID3ReaderException {
|
||||
private static List<Chapter> readChaptersFrom(CountingInputStream in) throws IOException, ID3ReaderException {
|
||||
ChapterReader reader = new ChapterReader();
|
||||
reader.readInputStream(in);
|
||||
List<Chapter> chapters = reader.getChapters();
|
||||
|
@ -0,0 +1,73 @@
|
||||
package de.danoeh.antennapod.core.util;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import de.danoeh.antennapod.core.util.playback.Playable;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public class EmbeddedChapterImage {
|
||||
private static final Pattern EMBEDDED_IMAGE_MATCHER = Pattern.compile("embedded-image://(\\d+)/(\\d+)");
|
||||
|
||||
private final int position;
|
||||
private final int length;
|
||||
private final String imageUrl;
|
||||
private final Playable media;
|
||||
|
||||
public EmbeddedChapterImage(Playable media, String imageUrl) {
|
||||
this.media = media;
|
||||
this.imageUrl = imageUrl;
|
||||
Matcher m = EMBEDDED_IMAGE_MATCHER.matcher(imageUrl);
|
||||
if (m.find()) {
|
||||
this.position = Integer.parseInt(m.group(1));
|
||||
this.length = Integer.parseInt(m.group(2));
|
||||
} else {
|
||||
throw new IllegalArgumentException("Not an embedded chapter");
|
||||
}
|
||||
}
|
||||
|
||||
public static String makeUrl(int position, int length) {
|
||||
return "embedded-image://" + position + "/" + length;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
public int getLength() {
|
||||
return length;
|
||||
}
|
||||
|
||||
public Playable getMedia() {
|
||||
return media;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
EmbeddedChapterImage that = (EmbeddedChapterImage) o;
|
||||
return TextUtils.equals(imageUrl, that.imageUrl);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return imageUrl.hashCode();
|
||||
}
|
||||
|
||||
private static boolean isEmbeddedChapterImage(String imageUrl) {
|
||||
return EMBEDDED_IMAGE_MATCHER.matcher(imageUrl).matches();
|
||||
}
|
||||
|
||||
public static Object getModelFor(Playable media, int chapter) {
|
||||
String imageUrl = media.getChapters().get(chapter).getImageUrl();
|
||||
if (isEmbeddedChapterImage(imageUrl)) {
|
||||
return new EmbeddedChapterImage(media, imageUrl);
|
||||
} else {
|
||||
return imageUrl;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,124 +1,154 @@
|
||||
package de.danoeh.antennapod.core.util.id3reader;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import de.danoeh.antennapod.core.feed.Chapter;
|
||||
import de.danoeh.antennapod.core.feed.ID3Chapter;
|
||||
import de.danoeh.antennapod.core.util.EmbeddedChapterImage;
|
||||
import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
|
||||
import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URLDecoder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import org.apache.commons.io.input.CountingInputStream;
|
||||
|
||||
public class ChapterReader extends ID3Reader {
|
||||
private static final String TAG = "ID3ChapterReader";
|
||||
|
||||
private static final String FRAME_ID_CHAPTER = "CHAP";
|
||||
private static final String FRAME_ID_TITLE = "TIT2";
|
||||
private static final String FRAME_ID_CHAPTER = "CHAP";
|
||||
private static final String FRAME_ID_TITLE = "TIT2";
|
||||
private static final String FRAME_ID_LINK = "WXXX";
|
||||
private static final String FRAME_ID_PICTURE = "APIC";
|
||||
private static final int IMAGE_TYPE_COVER = 3;
|
||||
|
||||
private List<Chapter> chapters;
|
||||
private ID3Chapter currentChapter;
|
||||
private List<Chapter> chapters;
|
||||
private ID3Chapter currentChapter;
|
||||
|
||||
@Override
|
||||
public int onStartTagHeader(TagHeader header) {
|
||||
chapters = new ArrayList<>();
|
||||
Log.d(TAG, "header: " + header);
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
}
|
||||
@Override
|
||||
public int onStartTagHeader(TagHeader header) {
|
||||
chapters = new ArrayList<>();
|
||||
Log.d(TAG, "header: " + header);
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartFrameHeader(FrameHeader header, InputStream input)
|
||||
throws IOException, ID3ReaderException {
|
||||
Log.d(TAG, "header: " + header);
|
||||
switch (header.getId()) {
|
||||
case FRAME_ID_CHAPTER:
|
||||
if (currentChapter != null) {
|
||||
if (!hasId3Chapter(currentChapter)) {
|
||||
chapters.add(currentChapter);
|
||||
Log.d(TAG, "Found chapter: " + currentChapter);
|
||||
currentChapter = null;
|
||||
}
|
||||
}
|
||||
StringBuilder elementId = new StringBuilder();
|
||||
readISOString(elementId, input, Integer.MAX_VALUE);
|
||||
char[] startTimeSource = readBytes(input, 4);
|
||||
long startTime = ((int) startTimeSource[0] << 24)
|
||||
| ((int) startTimeSource[1] << 16)
|
||||
| ((int) startTimeSource[2] << 8) | startTimeSource[3];
|
||||
currentChapter = new ID3Chapter(elementId.toString(), startTime);
|
||||
skipBytes(input, 12);
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
case FRAME_ID_TITLE:
|
||||
if (currentChapter != null && currentChapter.getTitle() == null) {
|
||||
StringBuilder title = new StringBuilder();
|
||||
readString(title, input, header.getSize());
|
||||
currentChapter
|
||||
.setTitle(title.toString());
|
||||
Log.d(TAG, "Found title: " + currentChapter.getTitle());
|
||||
@Override
|
||||
public int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException {
|
||||
Log.d(TAG, "header: " + header);
|
||||
switch (header.getId()) {
|
||||
case FRAME_ID_CHAPTER:
|
||||
if (currentChapter != null) {
|
||||
if (!hasId3Chapter(currentChapter)) {
|
||||
chapters.add(currentChapter);
|
||||
Log.d(TAG, "Found chapter: " + currentChapter);
|
||||
currentChapter = null;
|
||||
}
|
||||
}
|
||||
StringBuilder elementId = new StringBuilder();
|
||||
readISOString(elementId, input, Integer.MAX_VALUE);
|
||||
char[] startTimeSource = readChars(input, 4);
|
||||
long startTime = ((int) startTimeSource[0] << 24)
|
||||
| ((int) startTimeSource[1] << 16)
|
||||
| ((int) startTimeSource[2] << 8) | startTimeSource[3];
|
||||
currentChapter = new ID3Chapter(elementId.toString(), startTime);
|
||||
skipBytes(input, 12);
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
case FRAME_ID_TITLE:
|
||||
if (currentChapter != null && currentChapter.getTitle() == null) {
|
||||
StringBuilder title = new StringBuilder();
|
||||
readString(title, input, header.getSize());
|
||||
currentChapter
|
||||
.setTitle(title.toString());
|
||||
Log.d(TAG, "Found title: " + currentChapter.getTitle());
|
||||
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
}
|
||||
break;
|
||||
case FRAME_ID_LINK:
|
||||
if (currentChapter != null) {
|
||||
// skip description
|
||||
int descriptionLength = readString(null, input, header.getSize());
|
||||
StringBuilder link = new StringBuilder();
|
||||
readISOString(link, input, header.getSize() - descriptionLength);
|
||||
try {
|
||||
String decodedLink = URLDecoder.decode(link.toString(), "UTF-8");
|
||||
currentChapter.setLink(decodedLink);
|
||||
Log.d(TAG, "Found link: " + currentChapter.getLink());
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Log.w(TAG, "Bad URL found in ID3 data");
|
||||
}
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
}
|
||||
break;
|
||||
case FRAME_ID_LINK:
|
||||
if (currentChapter != null) {
|
||||
// skip description
|
||||
int descriptionLength = readString(null, input, header.getSize());
|
||||
StringBuilder link = new StringBuilder();
|
||||
readISOString(link, input, header.getSize() - descriptionLength);
|
||||
try {
|
||||
String decodedLink = URLDecoder.decode(link.toString(), "UTF-8");
|
||||
currentChapter.setLink(decodedLink);
|
||||
Log.d(TAG, "Found link: " + currentChapter.getLink());
|
||||
} catch (IllegalArgumentException iae) {
|
||||
Log.w(TAG, "Bad URL found in ID3 data");
|
||||
}
|
||||
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
}
|
||||
break;
|
||||
case "APIC":
|
||||
Log.d(TAG, header.toString());
|
||||
break;
|
||||
}
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
}
|
||||
break;
|
||||
case FRAME_ID_PICTURE:
|
||||
if (currentChapter != null) {
|
||||
Log.d(TAG, header.toString());
|
||||
StringBuilder mime = new StringBuilder();
|
||||
int read = readString(mime, input, header.getSize());
|
||||
byte type = (byte) readChars(input, 1)[0];
|
||||
read++;
|
||||
StringBuilder description = new StringBuilder();
|
||||
read += readISOString(description, input, header.getSize()); // Should use same encoding as mime
|
||||
|
||||
return super.onStartFrameHeader(header, input);
|
||||
}
|
||||
Log.d(TAG, "Found apic: " + mime + "," + description);
|
||||
if (mime.toString().equals("-->")) {
|
||||
// Data contains a link to a picture
|
||||
StringBuilder link = new StringBuilder();
|
||||
readISOString(link, input, header.getSize());
|
||||
Log.d(TAG, "link: " + link.toString());
|
||||
if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
|
||||
currentChapter.setImageUrl(link.toString());
|
||||
}
|
||||
} else {
|
||||
// Data contains the picture
|
||||
int length = header.getSize() - read;
|
||||
if (TextUtils.isEmpty(currentChapter.getImageUrl()) || type == IMAGE_TYPE_COVER) {
|
||||
currentChapter.setImageUrl(EmbeddedChapterImage.makeUrl(input.getCount(), length));
|
||||
}
|
||||
skipBytes(input, length);
|
||||
}
|
||||
return ID3Reader.ACTION_DONT_SKIP;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
private boolean hasId3Chapter(ID3Chapter chapter) {
|
||||
for (Chapter c : chapters) {
|
||||
if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return super.onStartFrameHeader(header, input);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEndTag() {
|
||||
if (currentChapter != null) {
|
||||
if (!hasId3Chapter(currentChapter)) {
|
||||
chapters.add(currentChapter);
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Reached end of tag");
|
||||
if (chapters != null) {
|
||||
for (Chapter c : chapters) {
|
||||
Log.d(TAG, "chapter: " + c);
|
||||
}
|
||||
}
|
||||
}
|
||||
private boolean hasId3Chapter(ID3Chapter chapter) {
|
||||
for (Chapter c : chapters) {
|
||||
if (((ID3Chapter) c).getId3ID().equals(chapter.getId3ID())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoTagHeaderFound() {
|
||||
Log.d(TAG, "No tag header found");
|
||||
super.onNoTagHeaderFound();
|
||||
}
|
||||
@Override
|
||||
public void onEndTag() {
|
||||
if (currentChapter != null) {
|
||||
if (!hasId3Chapter(currentChapter)) {
|
||||
chapters.add(currentChapter);
|
||||
}
|
||||
}
|
||||
Log.d(TAG, "Reached end of tag");
|
||||
if (chapters != null) {
|
||||
for (Chapter c : chapters) {
|
||||
Log.d(TAG, "chapter: " + c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<Chapter> getChapters() {
|
||||
return chapters;
|
||||
}
|
||||
@Override
|
||||
public void onNoTagHeaderFound() {
|
||||
Log.d(TAG, "No tag header found");
|
||||
super.onNoTagHeaderFound();
|
||||
}
|
||||
|
||||
public List<Chapter> getChapters() {
|
||||
return chapters;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import java.nio.charset.Charset;
|
||||
|
||||
import de.danoeh.antennapod.core.util.id3reader.model.FrameHeader;
|
||||
import de.danoeh.antennapod.core.util.id3reader.model.TagHeader;
|
||||
import org.apache.commons.io.input.CountingInputStream;
|
||||
|
||||
/**
|
||||
* Reads the ID3 Tag of a given file. In order to use this class, you should
|
||||
@ -33,11 +34,10 @@ public class ID3Reader {
|
||||
ID3Reader() {
|
||||
}
|
||||
|
||||
public final void readInputStream(InputStream input) throws IOException,
|
||||
ID3ReaderException {
|
||||
public final void readInputStream(CountingInputStream input) throws IOException, ID3ReaderException {
|
||||
int rc;
|
||||
readerPosition = 0;
|
||||
char[] tagHeaderSource = readBytes(input, HEADER_LENGTH);
|
||||
char[] tagHeaderSource = readChars(input, HEADER_LENGTH);
|
||||
tagHeader = createTagHeader(tagHeaderSource);
|
||||
if (tagHeader == null) {
|
||||
onNoTagHeaderFound();
|
||||
@ -47,7 +47,7 @@ public class ID3Reader {
|
||||
onEndTag();
|
||||
} else {
|
||||
while (readerPosition < tagHeader.getSize()) {
|
||||
FrameHeader frameHeader = createFrameHeader(readBytes(input, HEADER_LENGTH));
|
||||
FrameHeader frameHeader = createFrameHeader(readChars(input, HEADER_LENGTH));
|
||||
if (checkForNullString(frameHeader.getId())) {
|
||||
break;
|
||||
}
|
||||
@ -84,11 +84,10 @@ public class ID3Reader {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read a certain number of bytes from the given input stream. This method
|
||||
* Read a certain number of chars from the given input stream. This method
|
||||
* changes the readerPosition-attribute.
|
||||
*/
|
||||
char[] readBytes(InputStream input, int number)
|
||||
throws IOException, ID3ReaderException {
|
||||
char[] readChars(InputStream input, int number) throws IOException, ID3ReaderException {
|
||||
char[] header = new char[number];
|
||||
for (int i = 0; i < number; i++) {
|
||||
int b = input.read();
|
||||
@ -168,7 +167,7 @@ public class ID3Reader {
|
||||
protected int readString(StringBuilder buffer, InputStream input, int max) throws IOException,
|
||||
ID3ReaderException {
|
||||
if (max > 0) {
|
||||
char[] encoding = readBytes(input, 1);
|
||||
char[] encoding = readChars(input, 1);
|
||||
max--;
|
||||
|
||||
if (encoding[0] == ENCODING_UTF16_WITH_BOM || encoding[0] == ENCODING_UTF16_WITHOUT_BOM) {
|
||||
@ -230,8 +229,7 @@ public class ID3Reader {
|
||||
return ACTION_SKIP;
|
||||
}
|
||||
|
||||
int onStartFrameHeader(FrameHeader header, InputStream input)
|
||||
throws IOException, ID3ReaderException {
|
||||
int onStartFrameHeader(FrameHeader header, CountingInputStream input) throws IOException, ID3ReaderException {
|
||||
return ACTION_SKIP;
|
||||
}
|
||||
|
||||
|
@ -21,6 +21,7 @@ import android.widget.ImageButton;
|
||||
import android.widget.SeekBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import de.danoeh.antennapod.core.util.ThemeUtils;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
|
||||
import de.danoeh.antennapod.core.R;
|
||||
@ -397,12 +398,10 @@ public class PlaybackController {
|
||||
final CharSequence playText = activity.getString(R.string.play_label);
|
||||
final CharSequence pauseText = activity.getString(R.string.pause_label);
|
||||
|
||||
if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO ||
|
||||
PlaybackService.isCasting()) {
|
||||
TypedArray res = activity.obtainStyledAttributes(new int[]{
|
||||
R.attr.av_play_big, R.attr.av_pause_big});
|
||||
playResource = res.getResourceId(0, R.drawable.ic_play_arrow_grey600_36dp);
|
||||
pauseResource = res.getResourceId(1, R.drawable.ic_pause_grey600_36dp);
|
||||
if (PlaybackService.getCurrentMediaType() == MediaType.AUDIO || PlaybackService.isCasting()) {
|
||||
TypedArray res = activity.obtainStyledAttributes(new int[]{ R.attr.av_play, R.attr.av_pause});
|
||||
playResource = res.getResourceId(0, R.drawable.ic_av_play_dark_48dp);
|
||||
pauseResource = res.getResourceId(1, R.drawable.ic_av_pause_dark_48dp);
|
||||
res.recycle();
|
||||
} else {
|
||||
playResource = R.drawable.ic_av_play_white_80dp;
|
||||
@ -779,11 +778,8 @@ public class PlaybackController {
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(media -> {
|
||||
if (media.getMediaType() == MediaType.AUDIO) {
|
||||
TypedArray res = activity.obtainStyledAttributes(new int[]{
|
||||
de.danoeh.antennapod.core.R.attr.av_play_big});
|
||||
getPlayButton().setImageResource(
|
||||
res.getResourceId(0, de.danoeh.antennapod.core.R.drawable.ic_play_arrow_grey600_36dp));
|
||||
res.recycle();
|
||||
ThemeUtils.getDrawableFromAttr(activity, de.danoeh.antennapod.core.R.attr.av_play));
|
||||
} else {
|
||||
getPlayButton().setImageResource(R.drawable.ic_av_play_white_80dp);
|
||||
}
|
||||
|
@ -10,93 +10,91 @@ import de.danoeh.antennapod.core.feed.Chapter;
|
||||
import de.danoeh.antennapod.core.feed.VorbisCommentChapter;
|
||||
|
||||
public class VorbisCommentChapterReader extends VorbisCommentReader {
|
||||
private static final String TAG = "VorbisCommentChptrReadr";
|
||||
private static final String TAG = "VorbisCommentChptrReadr";
|
||||
|
||||
private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*";
|
||||
private static final String CHAPTER_ATTRIBUTE_TITLE = "name";
|
||||
private static final String CHAPTER_ATTRIBUTE_LINK = "url";
|
||||
private static final String CHAPTER_KEY = "chapter\\d\\d\\d.*";
|
||||
private static final String CHAPTER_ATTRIBUTE_TITLE = "name";
|
||||
private static final String CHAPTER_ATTRIBUTE_LINK = "url";
|
||||
|
||||
private List<Chapter> chapters;
|
||||
private List<Chapter> chapters;
|
||||
|
||||
public VorbisCommentChapterReader() {
|
||||
}
|
||||
public VorbisCommentChapterReader() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVorbisCommentFound() {
|
||||
System.out.println("Vorbis comment found");
|
||||
}
|
||||
@Override
|
||||
public void onVorbisCommentFound() {
|
||||
System.out.println("Vorbis comment found");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVorbisCommentHeaderFound(VorbisCommentHeader header) {
|
||||
chapters = new ArrayList<>();
|
||||
System.out.println(header.toString());
|
||||
}
|
||||
@Override
|
||||
public void onVorbisCommentHeaderFound(VorbisCommentHeader header) {
|
||||
chapters = new ArrayList<>();
|
||||
System.out.println(header.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onContentVectorKey(String content) {
|
||||
return content.matches(CHAPTER_KEY);
|
||||
}
|
||||
@Override
|
||||
public boolean onContentVectorKey(String content) {
|
||||
return content.matches(CHAPTER_KEY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onContentVectorValue(String key, String value)
|
||||
throws VorbisCommentReaderException {
|
||||
if (BuildConfig.DEBUG)
|
||||
Log.d(TAG, "Key: " + key + ", value: " + value);
|
||||
String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key);
|
||||
int id = VorbisCommentChapter.getIDFromKey(key);
|
||||
Chapter chapter = getChapterById(id);
|
||||
if (attribute == null) {
|
||||
if (getChapterById(id) == null) {
|
||||
// new chapter
|
||||
long start = VorbisCommentChapter.getStartTimeFromValue(value);
|
||||
chapter = new VorbisCommentChapter(id);
|
||||
chapter.setStart(start);
|
||||
chapters.add(chapter);
|
||||
} else {
|
||||
throw new VorbisCommentReaderException(
|
||||
"Found chapter with duplicate ID (" + key + ", "
|
||||
+ value + ")");
|
||||
}
|
||||
} else if (attribute.equals(CHAPTER_ATTRIBUTE_TITLE)) {
|
||||
if (chapter != null) {
|
||||
chapter.setTitle(value);
|
||||
}
|
||||
} else if (attribute.equals(CHAPTER_ATTRIBUTE_LINK)) {
|
||||
if (chapter != null) {
|
||||
chapter.setLink(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onContentVectorValue(String key, String value) throws VorbisCommentReaderException {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Key: " + key + ", value: " + value);
|
||||
}
|
||||
String attribute = VorbisCommentChapter.getAttributeTypeFromKey(key);
|
||||
int id = VorbisCommentChapter.getIDFromKey(key);
|
||||
Chapter chapter = getChapterById(id);
|
||||
if (attribute == null) {
|
||||
if (getChapterById(id) == null) {
|
||||
// new chapter
|
||||
long start = VorbisCommentChapter.getStartTimeFromValue(value);
|
||||
chapter = new VorbisCommentChapter(id);
|
||||
chapter.setStart(start);
|
||||
chapters.add(chapter);
|
||||
} else {
|
||||
throw new VorbisCommentReaderException("Found chapter with duplicate ID (" + key + ", " + value + ")");
|
||||
}
|
||||
} else if (attribute.equals(CHAPTER_ATTRIBUTE_TITLE)) {
|
||||
if (chapter != null) {
|
||||
chapter.setTitle(value);
|
||||
}
|
||||
} else if (attribute.equals(CHAPTER_ATTRIBUTE_LINK)) {
|
||||
if (chapter != null) {
|
||||
chapter.setLink(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoVorbisCommentFound() {
|
||||
System.out.println("No vorbis comment found");
|
||||
}
|
||||
@Override
|
||||
public void onNoVorbisCommentFound() {
|
||||
System.out.println("No vorbis comment found");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEndOfComment() {
|
||||
System.out.println("End of comment");
|
||||
for (Chapter c : chapters) {
|
||||
System.out.println(c.toString());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onEndOfComment() {
|
||||
System.out.println("End of comment");
|
||||
for (Chapter c : chapters) {
|
||||
System.out.println(c.toString());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(VorbisCommentReaderException exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
@Override
|
||||
public void onError(VorbisCommentReaderException exception) {
|
||||
exception.printStackTrace();
|
||||
}
|
||||
|
||||
private Chapter getChapterById(long id) {
|
||||
for (Chapter c : chapters) {
|
||||
if (((VorbisCommentChapter) c).getVorbisCommentId() == id) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
private Chapter getChapterById(long id) {
|
||||
for (Chapter c : chapters) {
|
||||
if (((VorbisCommentChapter) c).getVorbisCommentId() == id) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public List<Chapter> getChapters() {
|
||||
return chapters;
|
||||
}
|
||||
public List<Chapter> getChapters() {
|
||||
return chapters;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package de.danoeh.antennapod.core.util.vorbiscommentreader;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import org.apache.commons.io.EndianUtils;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
|
||||
@ -8,187 +9,158 @@ import java.io.InputStream;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.Arrays;
|
||||
|
||||
|
||||
public abstract class VorbisCommentReader {
|
||||
/** Length of first page in an ogg file in bytes. */
|
||||
private static final int FIRST_PAGE_LENGTH = 58;
|
||||
private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024;
|
||||
private static final int PACKET_TYPE_IDENTIFICATION = 1;
|
||||
private static final int PACKET_TYPE_COMMENT = 3;
|
||||
/** Length of first page in an ogg file in bytes. */
|
||||
private static final int FIRST_OGG_PAGE_LENGTH = 58;
|
||||
private static final int FIRST_OPUS_PAGE_LENGTH = 47;
|
||||
private static final int SECOND_PAGE_MAX_LENGTH = 64 * 1024 * 1024;
|
||||
private static final int PACKET_TYPE_IDENTIFICATION = 1;
|
||||
private static final int PACKET_TYPE_COMMENT = 3;
|
||||
|
||||
/** Called when Reader finds identification header. */
|
||||
protected abstract void onVorbisCommentFound();
|
||||
/** Called when Reader finds identification header. */
|
||||
protected abstract void onVorbisCommentFound();
|
||||
|
||||
protected abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header);
|
||||
protected abstract void onVorbisCommentHeaderFound(VorbisCommentHeader header);
|
||||
|
||||
/**
|
||||
* Is called every time the Reader finds a content vector. The handler
|
||||
* should return true if it wants to handle the content vector.
|
||||
*/
|
||||
protected abstract boolean onContentVectorKey(String content);
|
||||
/**
|
||||
* Is called every time the Reader finds a content vector. The handler
|
||||
* should return true if it wants to handle the content vector.
|
||||
*/
|
||||
protected abstract boolean onContentVectorKey(String content);
|
||||
|
||||
/**
|
||||
* Is called if onContentVectorKey returned true for the key.
|
||||
*
|
||||
* @throws VorbisCommentReaderException
|
||||
*/
|
||||
protected abstract void onContentVectorValue(String key, String value)
|
||||
throws VorbisCommentReaderException;
|
||||
/**
|
||||
* Is called if onContentVectorKey returned true for the key.
|
||||
*/
|
||||
protected abstract void onContentVectorValue(String key, String value) throws VorbisCommentReaderException;
|
||||
|
||||
protected abstract void onNoVorbisCommentFound();
|
||||
protected abstract void onNoVorbisCommentFound();
|
||||
|
||||
protected abstract void onEndOfComment();
|
||||
protected abstract void onEndOfComment();
|
||||
|
||||
protected abstract void onError(VorbisCommentReaderException exception);
|
||||
protected abstract void onError(VorbisCommentReaderException exception);
|
||||
|
||||
public void readInputStream(InputStream input)
|
||||
throws VorbisCommentReaderException {
|
||||
try {
|
||||
// look for identification header
|
||||
if (findIdentificationHeader(input)) {
|
||||
|
||||
onVorbisCommentFound();
|
||||
input = new OggInputStream(input);
|
||||
if (findCommentHeader(input)) {
|
||||
VorbisCommentHeader commentHeader = readCommentHeader(input);
|
||||
if (commentHeader != null) {
|
||||
onVorbisCommentHeaderFound(commentHeader);
|
||||
for (int i = 0; i < commentHeader
|
||||
.getUserCommentLength(); i++) {
|
||||
try {
|
||||
long vectorLength = EndianUtils
|
||||
.readSwappedUnsignedInteger(input);
|
||||
String key = readContentVectorKey(input,
|
||||
vectorLength).toLowerCase();
|
||||
boolean readValue = onContentVectorKey(key);
|
||||
if (readValue) {
|
||||
String value = readUTF8String(
|
||||
input,
|
||||
(int) (vectorLength - key.length() - 1));
|
||||
onContentVectorValue(key, value);
|
||||
} else {
|
||||
IOUtils.skipFully(input,
|
||||
vectorLength - key.length() - 1);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
onEndOfComment();
|
||||
}
|
||||
public void readInputStream(InputStream input) throws VorbisCommentReaderException {
|
||||
try {
|
||||
// look for identification header
|
||||
if (findIdentificationHeader(input)) {
|
||||
onVorbisCommentFound();
|
||||
input = new OggInputStream(input);
|
||||
if (findCommentHeader(input)) {
|
||||
VorbisCommentHeader commentHeader = readCommentHeader(input);
|
||||
onVorbisCommentHeaderFound(commentHeader);
|
||||
for (int i = 0; i < commentHeader.getUserCommentLength(); i++) {
|
||||
readUserComment(input);
|
||||
}
|
||||
onEndOfComment();
|
||||
} else {
|
||||
onError(new VorbisCommentReaderException("No comment header found"));
|
||||
}
|
||||
} else {
|
||||
onNoVorbisCommentFound();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
onError(new VorbisCommentReaderException(e));
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
onError(new VorbisCommentReaderException(
|
||||
"No comment header found"));
|
||||
}
|
||||
} else {
|
||||
onNoVorbisCommentFound();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
onError(new VorbisCommentReaderException(e));
|
||||
}
|
||||
}
|
||||
private void readUserComment(InputStream input) throws VorbisCommentReaderException {
|
||||
try {
|
||||
long vectorLength = EndianUtils.readSwappedUnsignedInteger(input);
|
||||
String key = readContentVectorKey(input, vectorLength).toLowerCase();
|
||||
boolean readValue = onContentVectorKey(key);
|
||||
if (readValue) {
|
||||
String value = readUtf8String(input, (int) (vectorLength - key.length() - 1));
|
||||
onContentVectorValue(key, value);
|
||||
} else {
|
||||
IOUtils.skipFully(input, vectorLength - key.length() - 1);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private String readUTF8String(InputStream input, long length)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[(int) length];
|
||||
|
||||
IOUtils.readFully(input, buffer);
|
||||
Charset charset = Charset.forName("UTF-8");
|
||||
return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString();
|
||||
}
|
||||
private String readUtf8String(InputStream input, long length) throws IOException {
|
||||
byte[] buffer = new byte[(int) length];
|
||||
IOUtils.readFully(input, buffer);
|
||||
Charset charset = Charset.forName("UTF-8");
|
||||
return charset.newDecoder().decode(ByteBuffer.wrap(buffer)).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks for an identification header in the first page of the file. If an
|
||||
* identification header is found, it will be skipped completely and the
|
||||
* method will return true, otherwise false.
|
||||
*
|
||||
* @throws IOException
|
||||
*/
|
||||
private boolean findIdentificationHeader(InputStream input)
|
||||
throws IOException {
|
||||
byte[] buffer = new byte[FIRST_PAGE_LENGTH];
|
||||
IOUtils.readFully(input, buffer);
|
||||
int i;
|
||||
for (i = 6; i < buffer.length; i++) {
|
||||
if (buffer[i - 5] == 'v' && buffer[i - 4] == 'o'
|
||||
&& buffer[i - 3] == 'r' && buffer[i - 2] == 'b'
|
||||
&& buffer[i - 1] == 'i' && buffer[i] == 's'
|
||||
&& buffer[i - 6] == PACKET_TYPE_IDENTIFICATION) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
/**
|
||||
* Looks for an identification header in the first page of the file. If an
|
||||
* identification header is found, it will be skipped completely and the
|
||||
* method will return true, otherwise false.
|
||||
*/
|
||||
private boolean findIdentificationHeader(InputStream input) throws IOException {
|
||||
byte[] buffer = new byte[FIRST_OPUS_PAGE_LENGTH];
|
||||
IOUtils.readFully(input, buffer);
|
||||
final byte[] oggIdentificationHeader = new byte[]{ PACKET_TYPE_IDENTIFICATION, 'v', 'o', 'r', 'b', 'i', 's' };
|
||||
for (int i = 6; i < buffer.length; i++) {
|
||||
if (bufferMatches(buffer, oggIdentificationHeader, i)) {
|
||||
IOUtils.skip(input, FIRST_OGG_PAGE_LENGTH - FIRST_OPUS_PAGE_LENGTH);
|
||||
return true;
|
||||
} else if (bufferMatches(buffer, "OpusHead".getBytes(), i)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean findCommentHeader(InputStream input) throws IOException {
|
||||
char[] buffer = new char["vorbis".length() + 1];
|
||||
for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
|
||||
char c = (char) input.read();
|
||||
int dest = -1;
|
||||
switch (c) {
|
||||
case PACKET_TYPE_COMMENT:
|
||||
dest = 0;
|
||||
break;
|
||||
case 'v':
|
||||
dest = 1;
|
||||
break;
|
||||
case 'o':
|
||||
dest = 2;
|
||||
break;
|
||||
case 'r':
|
||||
dest = 3;
|
||||
break;
|
||||
case 'b':
|
||||
dest = 4;
|
||||
break;
|
||||
case 'i':
|
||||
dest = 5;
|
||||
break;
|
||||
case 's':
|
||||
dest = 6;
|
||||
break;
|
||||
}
|
||||
if (dest >= 0) {
|
||||
buffer[dest] = c;
|
||||
if (buffer[1] == 'v' && buffer[2] == 'o' && buffer[3] == 'r'
|
||||
&& buffer[4] == 'b' && buffer[5] == 'i'
|
||||
&& buffer[6] == 's' && buffer[0] == PACKET_TYPE_COMMENT) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
Arrays.fill(buffer, (char) 0);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private boolean findCommentHeader(InputStream input) throws IOException {
|
||||
byte[] buffer = new byte[64]; // Enough space for some bytes. Used circularly.
|
||||
final byte[] oggCommentHeader = new byte[]{ PACKET_TYPE_COMMENT, 'v', 'o', 'r', 'b', 'i', 's' };
|
||||
for (int bytesRead = 0; bytesRead < SECOND_PAGE_MAX_LENGTH; bytesRead++) {
|
||||
buffer[bytesRead % buffer.length] = (byte) input.read();
|
||||
if (bufferMatches(buffer, oggCommentHeader, bytesRead)) {
|
||||
return true;
|
||||
} else if (bufferMatches(buffer, "OpusTags".getBytes(), bytesRead)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private VorbisCommentHeader readCommentHeader(InputStream input)
|
||||
throws IOException, VorbisCommentReaderException {
|
||||
try {
|
||||
long vendorLength = EndianUtils.readSwappedUnsignedInteger(input);
|
||||
String vendorName = readUTF8String(input, vendorLength);
|
||||
long userCommentLength = EndianUtils
|
||||
.readSwappedUnsignedInteger(input);
|
||||
return new VorbisCommentHeader(vendorName, userCommentLength);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new VorbisCommentReaderException(e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Reads backwards in haystack, starting at position. Checks if the bytes match needle.
|
||||
* Uses haystack circularly, so when reading at (-1), it reads at (length - 1).
|
||||
*/
|
||||
boolean bufferMatches(byte[] haystack, byte[] needle, int position) {
|
||||
for (int i = 0; i < needle.length; i++) {
|
||||
int posInHaystack = position - i;
|
||||
while (posInHaystack < 0) {
|
||||
posInHaystack += haystack.length;
|
||||
}
|
||||
posInHaystack = posInHaystack % haystack.length;
|
||||
if (haystack[posInHaystack] != needle[needle.length - 1 - i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private String readContentVectorKey(InputStream input, long vectorLength)
|
||||
throws IOException {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < vectorLength; i++) {
|
||||
char c = (char) input.read();
|
||||
if (c == '=') {
|
||||
return builder.toString();
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
return null; // no key found
|
||||
}
|
||||
@NonNull
|
||||
private VorbisCommentHeader readCommentHeader(InputStream input) throws IOException, VorbisCommentReaderException {
|
||||
try {
|
||||
long vendorLength = EndianUtils.readSwappedUnsignedInteger(input);
|
||||
String vendorName = readUtf8String(input, vendorLength);
|
||||
long userCommentLength = EndianUtils.readSwappedUnsignedInteger(input);
|
||||
return new VorbisCommentHeader(vendorName, userCommentLength);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new VorbisCommentReaderException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String readContentVectorKey(InputStream input, long vectorLength) throws IOException {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < vectorLength; i++) {
|
||||
char c = (char) input.read();
|
||||
if (c == '=') {
|
||||
return builder.toString();
|
||||
} else {
|
||||
builder.append(c);
|
||||
}
|
||||
}
|
||||
return null; // no key found
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 256 B |
Before Width: | Height: | Size: 324 B |
Before Width: | Height: | Size: 253 B |
Before Width: | Height: | Size: 315 B |
Before Width: | Height: | Size: 267 B |
Before Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 261 B |
Before Width: | Height: | Size: 321 B |
Before Width: | Height: | Size: 103 B |
Before Width: | Height: | Size: 126 B |
Before Width: | Height: | Size: 103 B |
Before Width: | Height: | Size: 124 B |
Before Width: | Height: | Size: 195 B |
Before Width: | Height: | Size: 235 B |
Before Width: | Height: | Size: 194 B |
Before Width: | Height: | Size: 232 B |
Before Width: | Height: | Size: 256 B |
Before Width: | Height: | Size: 251 B |
Before Width: | Height: | Size: 162 B |
Before Width: | Height: | Size: 256 B |
Before Width: | Height: | Size: 163 B |
Before Width: | Height: | Size: 253 B |
Before Width: | Height: | Size: 167 B |
Before Width: | Height: | Size: 267 B |
Before Width: | Height: | Size: 162 B |
Before Width: | Height: | Size: 261 B |
Before Width: | Height: | Size: 84 B |
Before Width: | Height: | Size: 103 B |
Before Width: | Height: | Size: 83 B |
Before Width: | Height: | Size: 103 B |
Before Width: | Height: | Size: 151 B |
Before Width: | Height: | Size: 195 B |
Before Width: | Height: | Size: 154 B |
Before Width: | Height: | Size: 194 B |
Before Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 183 B |
Before Width: | Height: | Size: 252 B |
Before Width: | Height: | Size: 332 B |
Before Width: | Height: | Size: 253 B |
Before Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 279 B |
Before Width: | Height: | Size: 348 B |
Before Width: | Height: | Size: 263 B |
Before Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 90 B |
Before Width: | Height: | Size: 92 B |
Before Width: | Height: | Size: 211 B |
Before Width: | Height: | Size: 270 B |
Before Width: | Height: | Size: 206 B |
Before Width: | Height: | Size: 270 B |
Before Width: | Height: | Size: 285 B |
Before Width: | Height: | Size: 285 B |
Before Width: | Height: | Size: 332 B |
Before Width: | Height: | Size: 520 B |
Before Width: | Height: | Size: 327 B |
Before Width: | Height: | Size: 518 B |
Before Width: | Height: | Size: 348 B |
Before Width: | Height: | Size: 547 B |
Before Width: | Height: | Size: 331 B |
Before Width: | Height: | Size: 548 B |
Before Width: | Height: | Size: 109 B |
Before Width: | Height: | Size: 143 B |
Before Width: | Height: | Size: 92 B |
Before Width: | Height: | Size: 143 B |
Before Width: | Height: | Size: 270 B |
Before Width: | Height: | Size: 367 B |
Before Width: | Height: | Size: 270 B |
Before Width: | Height: | Size: 351 B |
Before Width: | Height: | Size: 369 B |
Before Width: | Height: | Size: 361 B |