Merge pull request #9538 from Jared234/4186_warning_duplicates_in_playlist

Handle duplicate streams in the "Add to playlist" dialog
This commit is contained in:
Stypox 2023-01-29 10:36:31 +01:00 committed by GitHub
commit ca421c28a1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 121 additions and 10 deletions

View File

@ -0,0 +1,24 @@
package org.schabi.newpipe.database.playlist;
import androidx.room.ColumnInfo;
/**
* This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing
* how many times a specific stream is already contained inside a local playlist. Used to be able
* to grey out playlists which already contain the current stream in the playlist append dialog.
* @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String)
*/
public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained";
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
public final long timesStreamIsContained;
public PlaylistDuplicatesEntry(final long uid,
final String name,
final String thumbnailUrl,
final long streamCount,
final long timesStreamIsContained) {
super(uid, name, thumbnailUrl, streamCount);
this.timesStreamIsContained = timesStreamIsContained;
}
}

View File

@ -6,6 +6,7 @@ import androidx.room.RewriteQueriesToDropUnusedColumns;
import androidx.room.Transaction; import androidx.room.Transaction;
import org.schabi.newpipe.database.BasicDAO; import org.schabi.newpipe.database.BasicDAO;
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity; import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
@ -14,6 +15,7 @@ import java.util.List;
import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Flowable;
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT; import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME; import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
@ -26,6 +28,7 @@ import static org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity.PL
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL; import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL;
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE; import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@ -93,4 +96,24 @@ public interface PlaylistStreamDAO extends BasicDAO<PlaylistStreamEntity> {
+ " GROUP BY " + PLAYLIST_ID + " GROUP BY " + PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC") + " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata(); Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
@Transaction
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
+ PLAYLIST_NAME + ", "
+ PLAYLIST_TABLE + "." + PLAYLIST_THUMBNAIL_URL + ", "
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", "
+ "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS "
+ PLAYLIST_TIMES_STREAM_IS_CONTAINED
+ " FROM " + PLAYLIST_TABLE
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
+ " LEFT JOIN " + STREAM_TABLE
+ " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID
+ " AND :streamUrl = :streamUrl"
+ " GROUP BY " + JOIN_PLAYLIST_ID
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
} }

View File

@ -4,6 +4,7 @@ import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -13,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.schabi.newpipe.NewPipeDatabase; import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.stream.model.StreamEntity; import org.schabi.newpipe.database.stream.model.StreamEntity;
import org.schabi.newpipe.local.LocalItemListAdapter; import org.schabi.newpipe.local.LocalItemListAdapter;
import org.schabi.newpipe.local.playlist.LocalPlaylistManager; import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
@ -28,6 +29,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
private RecyclerView playlistRecyclerView; private RecyclerView playlistRecyclerView;
private LocalItemListAdapter playlistAdapter; private LocalItemListAdapter playlistAdapter;
private TextView playlistDuplicateIndicator;
private final CompositeDisposable playlistDisposables = new CompositeDisposable(); private final CompositeDisposable playlistDisposables = new CompositeDisposable();
@ -63,8 +65,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
playlistAdapter = new LocalItemListAdapter(getActivity()); playlistAdapter = new LocalItemListAdapter(getActivity());
playlistAdapter.setSelectedListener(selectedItem -> { playlistAdapter.setSelectedListener(selectedItem -> {
final List<StreamEntity> entities = getStreamEntities(); final List<StreamEntity> entities = getStreamEntities();
if (selectedItem instanceof PlaylistMetadataEntry && entities != null) { if (selectedItem instanceof PlaylistDuplicatesEntry && entities != null) {
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities); onPlaylistSelected(playlistManager,
(PlaylistDuplicatesEntry) selectedItem, entities);
} }
}); });
@ -72,10 +75,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext())); playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
playlistRecyclerView.setAdapter(playlistAdapter); playlistRecyclerView.setAdapter(playlistAdapter);
playlistDuplicateIndicator = view.findViewById(R.id.playlist_duplicate);
final View newPlaylistButton = view.findViewById(R.id.newPlaylist); final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog()); newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
playlistDisposables.add(playlistManager.getPlaylists() playlistDisposables.add(playlistManager
.getPlaylistDuplicates(getStreamEntities().get(0).getUrl())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(this::onPlaylistsReceived)); .subscribe(this::onPlaylistsReceived));
} }
@ -117,19 +123,36 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
requireDialog().dismiss(); requireDialog().dismiss();
} }
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) { private void onPlaylistsReceived(@NonNull final List<PlaylistDuplicatesEntry> playlists) {
if (playlistAdapter != null && playlistRecyclerView != null) { if (playlistAdapter != null
&& playlistRecyclerView != null
&& playlistDuplicateIndicator != null) {
playlistAdapter.clearStreamItemList(); playlistAdapter.clearStreamItemList();
playlistAdapter.addItems(playlists); playlistAdapter.addItems(playlists);
playlistRecyclerView.setVisibility(View.VISIBLE); playlistRecyclerView.setVisibility(View.VISIBLE);
playlistDuplicateIndicator.setVisibility(
anyPlaylistContainsDuplicates(playlists) ? View.VISIBLE : View.GONE);
} }
} }
private boolean anyPlaylistContainsDuplicates(final List<PlaylistDuplicatesEntry> playlists) {
return playlists.stream()
.anyMatch(playlist -> playlist.timesStreamIsContained > 0);
}
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager, private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
@NonNull final PlaylistMetadataEntry playlist, @NonNull final PlaylistDuplicatesEntry playlist,
@NonNull final List<StreamEntity> streams) { @NonNull final List<StreamEntity> streams) {
final Toast successToast = Toast.makeText(getContext(),
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT); final String toastText;
if (playlist.timesStreamIsContained > 0) {
toastText = getString(R.string.playlist_add_stream_success_duplicate,
playlist.timesStreamIsContained);
} else {
toastText = getString(R.string.playlist_add_stream_success);
}
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
if (playlist.thumbnailUrl if (playlist.thumbnailUrl
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) { .equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {

View File

@ -4,6 +4,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import org.schabi.newpipe.database.LocalItem; import org.schabi.newpipe.database.LocalItem;
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.local.LocalItemBuilder; import org.schabi.newpipe.local.LocalItemBuilder;
import org.schabi.newpipe.local.history.HistoryRecordManager; import org.schabi.newpipe.local.history.HistoryRecordManager;
@ -13,6 +14,9 @@ import org.schabi.newpipe.util.Localization;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
public class LocalPlaylistItemHolder extends PlaylistItemHolder { public class LocalPlaylistItemHolder extends PlaylistItemHolder {
private static final float GRAYED_OUT_ALPHA = 0.6f;
public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) { public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) {
super(infoItemBuilder, parent); super(infoItemBuilder, parent);
} }
@ -38,6 +42,13 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView); PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
if (item instanceof PlaylistDuplicatesEntry
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
itemView.setAlpha(GRAYED_OUT_ALPHA);
} else {
itemView.setAlpha(1.0f);
}
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter); super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
} }
} }

View File

@ -4,6 +4,7 @@ import androidx.annotation.Nullable;
import org.schabi.newpipe.R; import org.schabi.newpipe.R;
import org.schabi.newpipe.database.AppDatabase; import org.schabi.newpipe.database.AppDatabase;
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry; import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry; import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO; import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
@ -87,6 +88,18 @@ public class LocalPlaylistManager {
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io()); return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
} }
/**
* Get playlists with attached information about how many times the provided stream is already
* contained in each playlist.
*
* @param streamUrl the stream url for which to check for duplicates
* @return a list of {@link PlaylistDuplicatesEntry}
*/
public Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicates(final String streamUrl) {
return playlistStreamTable.getPlaylistDuplicatesMetadata(streamUrl)
.subscribeOn(Schedulers.io());
}
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) { public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io()); return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
} }

View File

@ -34,11 +34,26 @@
tools:ignore="RtlHardcoded" /> tools:ignore="RtlHardcoded" />
</RelativeLayout> </RelativeLayout>
<org.schabi.newpipe.views.NewPipeTextView
android:id="@+id/playlist_duplicate"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/newPlaylist"
android:layout_marginHorizontal="@dimen/video_item_search_padding"
android:layout_marginBottom="@dimen/video_item_search_padding"
android:gravity="center"
android:text="@string/duplicate_in_playlist"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="13sp"
android:visibility="gone"
tools:text="@tools:sample/lorem[20]"
tools:visibility="visible" />
<View <View
android:id="@+id/separator" android:id="@+id/separator"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_below="@+id/newPlaylist" android:layout_below="@+id/playlist_duplicate"
android:layout_marginLeft="@dimen/video_item_search_padding" android:layout_marginLeft="@dimen/video_item_search_padding"
android:layout_marginRight="@dimen/video_item_search_padding" android:layout_marginRight="@dimen/video_item_search_padding"
android:background="?attr/separator_color" /> android:background="?attr/separator_color" />

View File

@ -433,6 +433,7 @@
<string name="preferred_player_fetcher_notification_message">"Loading requested content"</string> <string name="preferred_player_fetcher_notification_message">"Loading requested content"</string>
<!-- Local Playlist --> <!-- Local Playlist -->
<string name="create_playlist">New Playlist</string> <string name="create_playlist">New Playlist</string>
<string name="duplicate_in_playlist">The playlists that are grayed out already contain this item.</string>
<string name="rename_playlist">Rename</string> <string name="rename_playlist">Rename</string>
<string name="name">Name</string> <string name="name">Name</string>
<string name="add_to_playlist">Add to playlist</string> <string name="add_to_playlist">Add to playlist</string>
@ -446,6 +447,7 @@
<string name="delete_playlist_prompt">Delete this playlist\?</string> <string name="delete_playlist_prompt">Delete this playlist\?</string>
<string name="playlist_creation_success">Playlist created</string> <string name="playlist_creation_success">Playlist created</string>
<string name="playlist_add_stream_success">Playlisted</string> <string name="playlist_add_stream_success">Playlisted</string>
<string name="playlist_add_stream_success_duplicate">Duplicate added %d time(s)</string>
<string name="playlist_thumbnail_change_success">Playlist thumbnail changed.</string> <string name="playlist_thumbnail_change_success">Playlist thumbnail changed.</string>
<string name="playlist_no_uploader">Auto-generated (no uploader found)</string> <string name="playlist_no_uploader">Auto-generated (no uploader found)</string>
<!-- Players --> <!-- Players -->