Skip to content

Commit ca421c2

Browse files
authored
Merge pull request #9538 from Jared234/4186_warning_duplicates_in_playlist
Handle duplicate streams in the "Add to playlist" dialog
2 parents 444ac5f + 711345e commit ca421c2

7 files changed

Lines changed: 121 additions & 10 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package org.schabi.newpipe.database.playlist;
2+
3+
import androidx.room.ColumnInfo;
4+
5+
/**
6+
* This class adds a field to {@link PlaylistMetadataEntry} that contains an integer representing
7+
* how many times a specific stream is already contained inside a local playlist. Used to be able
8+
* to grey out playlists which already contain the current stream in the playlist append dialog.
9+
* @see org.schabi.newpipe.local.playlist.LocalPlaylistManager#getPlaylistDuplicates(String)
10+
*/
11+
public class PlaylistDuplicatesEntry extends PlaylistMetadataEntry {
12+
public static final String PLAYLIST_TIMES_STREAM_IS_CONTAINED = "timesStreamIsContained";
13+
@ColumnInfo(name = PLAYLIST_TIMES_STREAM_IS_CONTAINED)
14+
public final long timesStreamIsContained;
15+
16+
public PlaylistDuplicatesEntry(final long uid,
17+
final String name,
18+
final String thumbnailUrl,
19+
final long streamCount,
20+
final long timesStreamIsContained) {
21+
super(uid, name, thumbnailUrl, streamCount);
22+
this.timesStreamIsContained = timesStreamIsContained;
23+
}
24+
}

app/src/main/java/org/schabi/newpipe/database/playlist/dao/PlaylistStreamDAO.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import androidx.room.Transaction;
77

88
import org.schabi.newpipe.database.BasicDAO;
9+
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
910
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
1011
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
1112
import org.schabi.newpipe.database.playlist.model.PlaylistStreamEntity;
@@ -14,6 +15,7 @@
1415

1516
import io.reactivex.rxjava3.core.Flowable;
1617

18+
import static org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry.PLAYLIST_TIMES_STREAM_IS_CONTAINED;
1719
import static org.schabi.newpipe.database.playlist.PlaylistMetadataEntry.PLAYLIST_STREAM_COUNT;
1820
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_ID;
1921
import static org.schabi.newpipe.database.playlist.model.PlaylistEntity.PLAYLIST_NAME;
@@ -26,6 +28,7 @@
2628
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_ID;
2729
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_TABLE;
2830
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_THUMBNAIL_URL;
31+
import static org.schabi.newpipe.database.stream.model.StreamEntity.STREAM_URL;
2932
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.JOIN_STREAM_ID_ALIAS;
3033
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_PROGRESS_MILLIS;
3134
import static org.schabi.newpipe.database.stream.model.StreamStateEntity.STREAM_STATE_TABLE;
@@ -93,4 +96,24 @@ default Flowable<List<PlaylistStreamEntity>> listByService(final int serviceId)
9396
+ " GROUP BY " + PLAYLIST_ID
9497
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
9598
Flowable<List<PlaylistMetadataEntry>> getPlaylistMetadata();
99+
100+
@Transaction
101+
@Query("SELECT " + PLAYLIST_TABLE + "." + PLAYLIST_ID + ", "
102+
+ PLAYLIST_NAME + ", "
103+
+ PLAYLIST_TABLE + "." + PLAYLIST_THUMBNAIL_URL + ", "
104+
+ "COALESCE(COUNT(" + JOIN_PLAYLIST_ID + "), 0) AS " + PLAYLIST_STREAM_COUNT + ", "
105+
+ "COALESCE(SUM(" + STREAM_URL + " = :streamUrl), 0) AS "
106+
+ PLAYLIST_TIMES_STREAM_IS_CONTAINED
107+
108+
+ " FROM " + PLAYLIST_TABLE
109+
+ " LEFT JOIN " + PLAYLIST_STREAM_JOIN_TABLE
110+
+ " ON " + PLAYLIST_TABLE + "." + PLAYLIST_ID + " = " + JOIN_PLAYLIST_ID
111+
112+
+ " LEFT JOIN " + STREAM_TABLE
113+
+ " ON " + STREAM_TABLE + "." + STREAM_ID + " = " + JOIN_STREAM_ID
114+
+ " AND :streamUrl = :streamUrl"
115+
116+
+ " GROUP BY " + JOIN_PLAYLIST_ID
117+
+ " ORDER BY " + PLAYLIST_NAME + " COLLATE NOCASE ASC")
118+
Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicatesMetadata(String streamUrl);
96119
}

app/src/main/java/org/schabi/newpipe/local/dialog/PlaylistAppendDialog.java

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import android.view.LayoutInflater;
55
import android.view.View;
66
import android.view.ViewGroup;
7+
import android.widget.TextView;
78
import android.widget.Toast;
89

910
import androidx.annotation.NonNull;
@@ -13,7 +14,7 @@
1314

1415
import org.schabi.newpipe.NewPipeDatabase;
1516
import org.schabi.newpipe.R;
16-
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
17+
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
1718
import org.schabi.newpipe.database.stream.model.StreamEntity;
1819
import org.schabi.newpipe.local.LocalItemListAdapter;
1920
import org.schabi.newpipe.local.playlist.LocalPlaylistManager;
@@ -28,6 +29,7 @@ public final class PlaylistAppendDialog extends PlaylistDialog {
2829

2930
private RecyclerView playlistRecyclerView;
3031
private LocalItemListAdapter playlistAdapter;
32+
private TextView playlistDuplicateIndicator;
3133

3234
private final CompositeDisposable playlistDisposables = new CompositeDisposable();
3335

@@ -63,19 +65,23 @@ public void onViewCreated(@NonNull final View view, @Nullable final Bundle saved
6365
playlistAdapter = new LocalItemListAdapter(getActivity());
6466
playlistAdapter.setSelectedListener(selectedItem -> {
6567
final List<StreamEntity> entities = getStreamEntities();
66-
if (selectedItem instanceof PlaylistMetadataEntry && entities != null) {
67-
onPlaylistSelected(playlistManager, (PlaylistMetadataEntry) selectedItem, entities);
68+
if (selectedItem instanceof PlaylistDuplicatesEntry && entities != null) {
69+
onPlaylistSelected(playlistManager,
70+
(PlaylistDuplicatesEntry) selectedItem, entities);
6871
}
6972
});
7073

7174
playlistRecyclerView = view.findViewById(R.id.playlist_list);
7275
playlistRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
7376
playlistRecyclerView.setAdapter(playlistAdapter);
7477

78+
playlistDuplicateIndicator = view.findViewById(R.id.playlist_duplicate);
79+
7580
final View newPlaylistButton = view.findViewById(R.id.newPlaylist);
7681
newPlaylistButton.setOnClickListener(ignored -> openCreatePlaylistDialog());
7782

78-
playlistDisposables.add(playlistManager.getPlaylists()
83+
playlistDisposables.add(playlistManager
84+
.getPlaylistDuplicates(getStreamEntities().get(0).getUrl())
7985
.observeOn(AndroidSchedulers.mainThread())
8086
.subscribe(this::onPlaylistsReceived));
8187
}
@@ -117,19 +123,36 @@ public void openCreatePlaylistDialog() {
117123
requireDialog().dismiss();
118124
}
119125

120-
private void onPlaylistsReceived(@NonNull final List<PlaylistMetadataEntry> playlists) {
121-
if (playlistAdapter != null && playlistRecyclerView != null) {
126+
private void onPlaylistsReceived(@NonNull final List<PlaylistDuplicatesEntry> playlists) {
127+
if (playlistAdapter != null
128+
&& playlistRecyclerView != null
129+
&& playlistDuplicateIndicator != null) {
122130
playlistAdapter.clearStreamItemList();
123131
playlistAdapter.addItems(playlists);
124132
playlistRecyclerView.setVisibility(View.VISIBLE);
133+
playlistDuplicateIndicator.setVisibility(
134+
anyPlaylistContainsDuplicates(playlists) ? View.VISIBLE : View.GONE);
125135
}
126136
}
127137

138+
private boolean anyPlaylistContainsDuplicates(final List<PlaylistDuplicatesEntry> playlists) {
139+
return playlists.stream()
140+
.anyMatch(playlist -> playlist.timesStreamIsContained > 0);
141+
}
142+
128143
private void onPlaylistSelected(@NonNull final LocalPlaylistManager manager,
129-
@NonNull final PlaylistMetadataEntry playlist,
144+
@NonNull final PlaylistDuplicatesEntry playlist,
130145
@NonNull final List<StreamEntity> streams) {
131-
final Toast successToast = Toast.makeText(getContext(),
132-
R.string.playlist_add_stream_success, Toast.LENGTH_SHORT);
146+
147+
final String toastText;
148+
if (playlist.timesStreamIsContained > 0) {
149+
toastText = getString(R.string.playlist_add_stream_success_duplicate,
150+
playlist.timesStreamIsContained);
151+
} else {
152+
toastText = getString(R.string.playlist_add_stream_success);
153+
}
154+
155+
final Toast successToast = Toast.makeText(getContext(), toastText, Toast.LENGTH_SHORT);
133156

134157
if (playlist.thumbnailUrl
135158
.equals("drawable://" + R.drawable.placeholder_thumbnail_playlist)) {

app/src/main/java/org/schabi/newpipe/local/holder/LocalPlaylistItemHolder.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import android.view.ViewGroup;
55

66
import org.schabi.newpipe.database.LocalItem;
7+
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
78
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
89
import org.schabi.newpipe.local.LocalItemBuilder;
910
import org.schabi.newpipe.local.history.HistoryRecordManager;
@@ -13,6 +14,9 @@
1314
import java.time.format.DateTimeFormatter;
1415

1516
public class LocalPlaylistItemHolder extends PlaylistItemHolder {
17+
18+
private static final float GRAYED_OUT_ALPHA = 0.6f;
19+
1620
public LocalPlaylistItemHolder(final LocalItemBuilder infoItemBuilder, final ViewGroup parent) {
1721
super(infoItemBuilder, parent);
1822
}
@@ -38,6 +42,13 @@ public void updateFromItem(final LocalItem localItem,
3842

3943
PicassoHelper.loadPlaylistThumbnail(item.thumbnailUrl).into(itemThumbnailView);
4044

45+
if (item instanceof PlaylistDuplicatesEntry
46+
&& ((PlaylistDuplicatesEntry) item).timesStreamIsContained > 0) {
47+
itemView.setAlpha(GRAYED_OUT_ALPHA);
48+
} else {
49+
itemView.setAlpha(1.0f);
50+
}
51+
4152
super.updateFromItem(localItem, historyRecordManager, dateTimeFormatter);
4253
}
4354
}

app/src/main/java/org/schabi/newpipe/local/playlist/LocalPlaylistManager.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import org.schabi.newpipe.R;
66
import org.schabi.newpipe.database.AppDatabase;
7+
import org.schabi.newpipe.database.playlist.PlaylistDuplicatesEntry;
78
import org.schabi.newpipe.database.playlist.PlaylistMetadataEntry;
89
import org.schabi.newpipe.database.playlist.PlaylistStreamEntry;
910
import org.schabi.newpipe.database.playlist.dao.PlaylistDAO;
@@ -87,6 +88,18 @@ public Flowable<List<PlaylistMetadataEntry>> getPlaylists() {
8788
return playlistStreamTable.getPlaylistMetadata().subscribeOn(Schedulers.io());
8889
}
8990

91+
/**
92+
* Get playlists with attached information about how many times the provided stream is already
93+
* contained in each playlist.
94+
*
95+
* @param streamUrl the stream url for which to check for duplicates
96+
* @return a list of {@link PlaylistDuplicatesEntry}
97+
*/
98+
public Flowable<List<PlaylistDuplicatesEntry>> getPlaylistDuplicates(final String streamUrl) {
99+
return playlistStreamTable.getPlaylistDuplicatesMetadata(streamUrl)
100+
.subscribeOn(Schedulers.io());
101+
}
102+
90103
public Flowable<List<PlaylistStreamEntry>> getPlaylistStreams(final long playlistId) {
91104
return playlistStreamTable.getOrderedStreamsOf(playlistId).subscribeOn(Schedulers.io());
92105
}

app/src/main/res/layout/dialog_playlists.xml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,26 @@
3434
tools:ignore="RtlHardcoded" />
3535
</RelativeLayout>
3636

37+
<org.schabi.newpipe.views.NewPipeTextView
38+
android:id="@+id/playlist_duplicate"
39+
android:layout_width="match_parent"
40+
android:layout_height="wrap_content"
41+
android:layout_below="@+id/newPlaylist"
42+
android:layout_marginHorizontal="@dimen/video_item_search_padding"
43+
android:layout_marginBottom="@dimen/video_item_search_padding"
44+
android:gravity="center"
45+
android:text="@string/duplicate_in_playlist"
46+
android:textAppearance="?android:attr/textAppearanceMedium"
47+
android:textSize="13sp"
48+
android:visibility="gone"
49+
tools:text="@tools:sample/lorem[20]"
50+
tools:visibility="visible" />
51+
3752
<View
3853
android:id="@+id/separator"
3954
android:layout_width="match_parent"
4055
android:layout_height="1dp"
41-
android:layout_below="@+id/newPlaylist"
56+
android:layout_below="@+id/playlist_duplicate"
4257
android:layout_marginLeft="@dimen/video_item_search_padding"
4358
android:layout_marginRight="@dimen/video_item_search_padding"
4459
android:background="?attr/separator_color" />

app/src/main/res/values/strings.xml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,7 @@
433433
<string name="preferred_player_fetcher_notification_message">"Loading requested content"</string>
434434
<!-- Local Playlist -->
435435
<string name="create_playlist">New Playlist</string>
436+
<string name="duplicate_in_playlist">The playlists that are grayed out already contain this item.</string>
436437
<string name="rename_playlist">Rename</string>
437438
<string name="name">Name</string>
438439
<string name="add_to_playlist">Add to playlist</string>
@@ -446,6 +447,7 @@
446447
<string name="delete_playlist_prompt">Delete this playlist\?</string>
447448
<string name="playlist_creation_success">Playlist created</string>
448449
<string name="playlist_add_stream_success">Playlisted</string>
450+
<string name="playlist_add_stream_success_duplicate">Duplicate added %d time(s)</string>
449451
<string name="playlist_thumbnail_change_success">Playlist thumbnail changed.</string>
450452
<string name="playlist_no_uploader">Auto-generated (no uploader found)</string>
451453
<!-- Players -->

0 commit comments

Comments
 (0)