mirror of
				https://github.com/TeamNewPipe/NewPipe
				synced 2025-11-04 01:03:00 +00:00 
			
		
		
		
	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:
		@@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,6 +6,7 @@ import androidx.room.RewriteQueriesToDropUnusedColumns;
 | 
			
		||||
import androidx.room.Transaction;
 | 
			
		||||
 | 
			
		||||
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.PlaylistStreamEntry;
 | 
			
		||||
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
 | 
			
		||||
@@ -14,6 +15,7 @@ import java.util.List;
 | 
			
		||||
 | 
			
		||||
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.model.PlaylistEntity.PLAYLIST_ID;
 | 
			
		||||
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_TABLE;
 | 
			
		||||
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.STREAM_PROGRESS_MILLIS;
 | 
			
		||||
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
 | 
			
		||||
            + " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
 | 
			
		||||
    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);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import android.os.Bundle;
 | 
			
		||||
import android.view.LayoutInflater;
 | 
			
		||||
import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
import android.widget.TextView;
 | 
			
		||||
import android.widget.Toast;
 | 
			
		||||
 | 
			
		||||
import androidx.annotation.NonNull;
 | 
			
		||||
@@ -13,7 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
 | 
			
		||||
 | 
			
		||||
import org.schabi.newpipe.NewPipeDatabase;
 | 
			
		||||
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.local.LocalItemListAdapter;
 | 
			
		||||
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
 | 
			
		||||
@@ -28,6 +29,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
 | 
			
		||||
 | 
			
		||||
    private RecyclerView playlistRecyclerView;
 | 
			
		||||
    private LocalItemListAdapter playlistAdapter;
 | 
			
		||||
    private TextView playlistDuplicateIndicator;
 | 
			
		||||
 | 
			
		||||
    private final CompositeDisposable playlistDisposables = new CompositeDisposable();
 | 
			
		||||
 | 
			
		||||
@@ -63,8 +65,9 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
 | 
			
		||||
        playlistAdapter = new LocalItemListAdapter(getActivity());
 | 
			
		||||
        playlistAdapter.setSelectedListener(selectedItem -> {
 | 
			
		||||
            final List<StreamEntity> entities = getStreamEntities();
 | 
			
		||||
            if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
 | 
			
		||||
                onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
 | 
			
		||||
            if (selectedItem instanceof PlaylistDuplicatesEntry && entities != null) {
 | 
			
		||||
                onPlaylistSelected(playlistManager,
 | 
			
		||||
                        (PlaylistDuplicatesEntry) selectedItem, entities);
 | 
			
		||||
            }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
@@ -72,10 +75,13 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
 | 
			
		||||
        playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
 | 
			
		||||
        playlistRecyclerView.setAdapter(playlistAdapter);
 | 
			
		||||
 | 
			
		||||
        playlistDuplicateIndicator = view.findViewById(R.id.playlist_duplicate);
 | 
			
		||||
 | 
			
		||||
        final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
 | 
			
		||||
        newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
 | 
			
		||||
 | 
			
		||||
        playlistDisposables.add(playlistManager.getPlaylists()
 | 
			
		||||
        playlistDisposables.add(playlistManager
 | 
			
		||||
                .getPlaylistDuplicates(getStreamEntities().get(0).getUrl())
 | 
			
		||||
                .observeOn(AndroidSchedulers.mainThread())
 | 
			
		||||
                .subscribe(this::onPlaylistsReceived));
 | 
			
		||||
    }
 | 
			
		||||
@@ -117,19 +123,36 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
 | 
			
		||||
        requireDialog().dismiss();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
 | 
			
		||||
        if (playlistAdapter != null && playlistRecyclerView != null) {
 | 
			
		||||
    private void onPlaylistsReceived(@NonNull final List<PlaylistDuplicatesEntry> playlists) {
 | 
			
		||||
        if (playlistAdapter != null
 | 
			
		||||
                && playlistRecyclerView != null
 | 
			
		||||
                && playlistDuplicateIndicator != null) {
 | 
			
		||||
            playlistAdapter.clearStreamItemList();
 | 
			
		||||
            playlistAdapter.addItems(playlists);
 | 
			
		||||
            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,
 | 
			
		||||
                                    @NonNull final PlaylistMetadataEntry playlist,
 | 
			
		||||
                                    @NonNull final PlaylistDuplicatesEntry playlist,
 | 
			
		||||
                                    @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
 | 
			
		||||
                .equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import android.view.View;
 | 
			
		||||
import android.view.ViewGroup;
 | 
			
		||||
 | 
			
		||||
import org.schabi.newpipe.database.LocalItem;
 | 
			
		||||
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
 | 
			
		||||
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
 | 
			
		||||
import org.schabi.newpipe.local.LocalItemBuilder;
 | 
			
		||||
import org.schabi.newpipe.local.history.HistoryRecordManager;
 | 
			
		||||
@@ -13,6 +14,9 @@ import org.schabi.newpipe.util.Localization;
 | 
			
		||||
import java.time.format.DateTimeFormatter;
 | 
			
		||||
 | 
			
		||||
public class LocalPlaylistItemHolder extends PlaylistItemHolder {
 | 
			
		||||
 | 
			
		||||
    private static final float GRAYED_OUT_ALPHA = 0.6f;
 | 
			
		||||
 | 
			
		||||
    public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) {
 | 
			
		||||
        super(infoItemBuilder, parent);
 | 
			
		||||
    }
 | 
			
		||||
@@ -38,6 +42,13 @@ public class LocalPlaylistItemHolder extends PlaylistItemHolder {
 | 
			
		||||
 | 
			
		||||
        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);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,7 @@ import androidx.annotation.Nullable;
 | 
			
		||||
 | 
			
		||||
import org.schabi.newpipe.R;
 | 
			
		||||
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.PlaylistStreamEntry;
 | 
			
		||||
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
 | 
			
		||||
@@ -87,6 +88,18 @@ public class LocalPlaylistManager {
 | 
			
		||||
        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) {
 | 
			
		||||
        return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -34,11 +34,26 @@
 | 
			
		||||
            tools:ignore="RtlHardcoded" />
 | 
			
		||||
    </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
 | 
			
		||||
        android:id="@+id/separator"
 | 
			
		||||
        android:layout_width="match_parent"
 | 
			
		||||
        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_marginRight="@dimen/video_item_search_padding"
 | 
			
		||||
        android:background="?attr/separator_color" />
 | 
			
		||||
 
 | 
			
		||||
@@ -433,6 +433,7 @@
 | 
			
		||||
    <string name="preferred_player_fetcher_notification_message">"Loading requested content"</string>
 | 
			
		||||
    <!-- Local Playlist -->
 | 
			
		||||
    <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="name">Name</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="playlist_creation_success">Playlist created</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_no_uploader">Auto-generated (no uploader found)</string>
 | 
			
		||||
    <!-- Players -->
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user