From 95ad141b8f84c5acb5f39197f937b9520e7875e0 Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Wed, 17 Jul 2024 12:26:50 +0200 Subject: [PATCH 01/10] Normalize subscription videos table Cleanup further --- .../ChromecastControllerFragment.java | 6 +- .../java/free/rm/skytube/app/SkyTubeApp.java | 2 +- .../YouTube/POJOs/PersistentChannel.java | 54 ++ .../YouTube/POJOs/YouTubeChannel.java | 34 +- .../businessobjects/YouTube/YouTubeTasks.java | 16 +- .../YouTube/newpipe/NewPipeService.java | 6 +- .../businessobjects/db/DatabaseTasks.java | 86 ++-- .../businessobjects/db/LocalChannelTable.java | 24 +- .../db/SQLiteOpenHelperEx.java | 9 + .../businessobjects/db/SubscriptionsDb.java | 474 ++++++++---------- .../db/SubscriptionsTable.java | 101 ++-- .../db/SubscriptionsVideosTable.java | 13 + .../SubscriptionsBackupsManager.java | 23 +- .../businessobjects/adapters/SubsAdapter.java | 2 +- .../views/ChannelActionHandler.java | 2 +- .../views/SubscribeButton.java | 2 +- .../gui/fragments/ChannelBrowserFragment.java | 4 +- .../fragments/YouTubePlayerV1Fragment.java | 2 +- .../fragments/YouTubePlayerV2Fragment.java | 2 +- 19 files changed, 472 insertions(+), 390 deletions(-) create mode 100644 app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java diff --git a/app/src/extra/java/free/rm/skytube/gui/fragments/ChromecastControllerFragment.java b/app/src/extra/java/free/rm/skytube/gui/fragments/ChromecastControllerFragment.java index bc398a5f4b..3546dbd44b 100644 --- a/app/src/extra/java/free/rm/skytube/gui/fragments/ChromecastControllerFragment.java +++ b/app/src/extra/java/free/rm/skytube/gui/fragments/ChromecastControllerFragment.java @@ -17,6 +17,7 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient; import free.rm.skytube.R; +import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; import free.rm.skytube.businessobjects.db.DatabaseTasks; import free.rm.skytube.databinding.FragmentChromecastControllerBinding; import free.rm.skytube.databinding.VideoDescriptionBinding; @@ -106,8 +107,9 @@ private void setupDescription() { compositeDisposable.add( DatabaseTasks.getChannelInfo(requireContext(), video.getChannelId(), false) - .subscribe(youTubeChannel -> { - videoDescriptionBinding.videoDescSubscribeButton.setChannel(youTubeChannel); + .subscribe(subscribedChannel -> { + YouTubeChannel youTubeChannel = subscribedChannel.channel(); + videoDescriptionBinding.videoDescSubscribeButton.setChannel(youTubeChannel); Glide.with(requireContext()) .load(youTubeChannel.getThumbnailUrl()) diff --git a/app/src/main/java/free/rm/skytube/app/SkyTubeApp.java b/app/src/main/java/free/rm/skytube/app/SkyTubeApp.java index 4e888f2904..90ca0ec9ee 100644 --- a/app/src/main/java/free/rm/skytube/app/SkyTubeApp.java +++ b/app/src/main/java/free/rm/skytube/app/SkyTubeApp.java @@ -516,7 +516,7 @@ public static boolean openContent(Context ctx, ContentId content) { public static void launchChannel(ChannelId channelId, Context context) { if (channelId != null) { DatabaseTasks.getChannelInfo(context, channelId, true) - .subscribe(youTubeChannel -> launchChannel(youTubeChannel, context)); + .subscribe(youTubeChannel -> launchChannel(youTubeChannel.channel(), context)); } } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java new file mode 100644 index 0000000000..8b65aa9d0f --- /dev/null +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java @@ -0,0 +1,54 @@ +/* + * SkyTube + * Copyright (C) 2024 Zsombor Gegesy + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation (version 3 of the License). + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package free.rm.skytube.businessobjects.YouTube.POJOs; + +public final class PersistentChannel { + final YouTubeChannel channel; + final long channelPk; + final Long subscriptionPk; + + public PersistentChannel(YouTubeChannel channel, long channelPk, Long subscriptionPk) { + this.channel = channel; + this.channelPk = channelPk; + this.subscriptionPk = subscriptionPk; + } + + public YouTubeChannel channel() { + return channel; + } + + public long channelPk() { + return channelPk; + } + + public Long subscriptionPk() { + return subscriptionPk; + } + + public boolean isSubscribed() { + return subscriptionPk != null; + } + + public PersistentChannel with(YouTubeChannel newInstance) { + newInstance.setUserSubscribed(isSubscribed()); + return new PersistentChannel(newInstance, channelPk, subscriptionPk); + } + + public PersistentChannel withSubscriptionPk(Long newSubscriptionPk) { + return new PersistentChannel(channel, channelPk, newSubscriptionPk); + } +} diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java index f3b0f2d268..0352eacb27 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java @@ -143,6 +143,10 @@ public long getLastVideoTime() { return lastVideoTime; } + public void setLastVideoTime(long lastVideoTime) { + this.lastVideoTime = lastVideoTime; + } + public List getTags() { return tags; } @@ -174,12 +178,10 @@ public Single blockChannel() { * out. */ public Single blockChannel(boolean displayToastMessage) { - return SubscriptionsDb.getSubscriptionsDb().getUserSubscribedToChannel(getChannelId()) - .flatMap(isSubscribed -> DatabaseTasks.subscribeToChannel(false, - null, SkyTubeApp.getContext(), this, false)) - .map(result -> SkyTubeApp.getSettings().isChannelDenyListEnabled()) - .flatMap(isDenyListEnabled -> { - if (isDenyListEnabled) { + return DatabaseTasks.subscribeToChannel(false, + null, SkyTubeApp.getContext(), getChannelId(), false) + .flatMap(result -> { + if (SkyTubeApp.getSettings().isChannelDenyListEnabled()) { return dennyChannel(displayToastMessage); } else { return removeAllowedChannel(displayToastMessage); @@ -196,17 +198,9 @@ public Single unblockChannel() { return unblockChannel(true); } - public Single unblockChannel(boolean displayToastMessage) { - return Single.fromCallable(() -> SkyTubeApp.getSettings().isChannelDenyListEnabled()) - .flatMap(isDenyListEnabled -> { - if (isDenyListEnabled) { - return removeDeniedChannel(displayToastMessage); - } else { - return allowChannel(displayToastMessage); - } - }) - .observeOn(AndroidSchedulers.mainThread()); - } + public Single unblockChannel(boolean displayToastMessage) { + return (SkyTubeApp.getSettings().isChannelDenyListEnabled() ? removeDeniedChannel(displayToastMessage) : allowChannel(displayToastMessage)).observeOn(AndroidSchedulers.mainThread()); + } /** * Denny the channel. @@ -296,14 +290,14 @@ public static Disposable subscribeChannel(final Context context, final ChannelId if (channelId != null) { return DatabaseTasks.getChannelInfo(context, channelId, false) .observeOn(Schedulers.io()) - .map(youTubeChannel -> - new Pair<>(youTubeChannel, SubscriptionsDb.getSubscriptionsDb().subscribe(youTubeChannel)) + .map(persistentChannel -> + new Pair<>(persistentChannel, SubscriptionsDb.getSubscriptionsDb().subscribe(persistentChannel, Collections.emptyList())) ) .observeOn(AndroidSchedulers.mainThread()) .subscribe(youTubeChannelWithResult -> { switch(youTubeChannelWithResult.second) { case SUCCESS: { - youTubeChannelWithResult.first.setUserSubscribed(true); + youTubeChannelWithResult.first.channel.setUserSubscribed(true); EventBus.getInstance().notifyMainTabChanged(EventBus.SettingChange.SUBSCRIPTION_LIST_CHANGED); SkyTubeApp.getSettings().setRefreshSubsFeedFromCache(true); Toast.makeText(context, R.string.channel_subscribed, Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java index 073ed1d451..389dd37af6 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java @@ -32,6 +32,7 @@ import free.rm.skytube.app.Utils; import free.rm.skytube.businessobjects.VideoCategory; import free.rm.skytube.businessobjects.YouTube.POJOs.CardData; +import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeAPIKey; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubePlaylist; @@ -148,7 +149,7 @@ private static Single getBulkSubscriptionVideos(@NonNull List newVideos = fetchVideos(subscriptionsDb, alreadyKnownVideos, channelId); List detailedList = new ArrayList<>(); if (!newVideos.isEmpty()) { - YouTubeChannel dbChannel = subscriptionsDb.getCachedSubscribedChannel(channelId); + PersistentChannel dbChannel = subscriptionsDb.getCachedChannel(channelId); for (YouTubeVideo vid : newVideos) { YouTubeVideo details; try { @@ -157,7 +158,7 @@ private static Single getBulkSubscriptionVideos(@NonNull List getBulkSubscriptionVideos(@NonNull List> getChannelVideos(@NonNull ChannelId ch realVideos.add((YouTubeVideo) cd); } } - db.saveVideos(realVideos, channelId); + PersistentChannel channel = db.getCachedChannel(channelId); + db.saveChannelVideos(realVideos, channel, true); return realVideos; }) .subscribeOn(Schedulers.io()) @@ -257,7 +259,7 @@ private static Single> getChannelVideos(@NonNull ChannelId ch .doOnError(throwable -> Toast.makeText(getContext(), String.format(getContext().getString(R.string.could_not_get_videos), - db.getCachedChannel(channelId).getTitle()), + db.getCachedChannel(channelId).channel().getTitle()), Toast.LENGTH_LONG).show() ); } @@ -437,7 +439,9 @@ public static Maybe> getYouTubeVideos(@NonNull GetYouTubeVideos g channel.addYouTubeVideo((YouTubeVideo) video); } } - SubscriptionsDb.getSubscriptionsDb().saveChannelVideos(channel.getYouTubeVideos(), channel.getChannelId()); + SubscriptionsDb db = SubscriptionsDb.getSubscriptionsDb(); + PersistentChannel persistentChannel = db.getCachedChannel(channel.getChannelId()); + db.saveChannelVideos(channel.getYouTubeVideos(), persistentChannel, false); } } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java index 593096653f..e0090c4c86 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java @@ -60,6 +60,7 @@ import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; import free.rm.skytube.BuildConfig; import free.rm.skytube.app.Settings; @@ -322,7 +323,10 @@ public YouTubeChannel getChannelDetails(ChannelId channelId) throws NewPipeExcep // get the channel, and add all the videos from the first page YouTubeChannel channel = pager.getChannel(); try { - channel.getYouTubeVideos().addAll(pager.getNextPageAsVideos()); + List videos = pager.getNextPageAsVideos(); + channel.getYouTubeVideos().addAll(videos); + long lastPublish = videos.stream().mapToLong(YouTubeVideo::getPublishTimestamp).max().orElse(0); + channel.setLastVideoTime(lastPublish); } catch (NewPipeException e) { Logger.e(this, "Unable to retrieve videos for "+channelId+", error: "+e.getMessage(), e); } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java index 8976c8b485..ccf2aef632 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java @@ -10,24 +10,25 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.util.Pair; +import java.util.Collections; import java.util.List; -import java.util.Objects; import free.rm.skytube.R; import free.rm.skytube.app.EventBus; import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.businessobjects.YouTube.POJOs.ChannelView; +import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeVideo; import free.rm.skytube.businessobjects.YouTube.VideoBlocker; import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; +import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeException; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeService; import free.rm.skytube.gui.businessobjects.views.ChannelSubscriber; -import free.rm.skytube.gui.businessobjects.views.SubscribeButton; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Completable; -import io.reactivex.rxjava3.core.Flowable; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; @@ -45,29 +46,10 @@ private DatabaseTasks() {} * Task to retrieve channel information - from the local cache, or from the remote service if the * value is old or doesn't exist. */ - public static Maybe getChannelInfo(@NonNull Context context, - @NonNull ChannelId channelId, - boolean staleAcceptable) { - return Maybe.fromCallable(() -> { - final SubscriptionsDb db = SubscriptionsDb.getSubscriptionsDb(); - YouTubeChannel channel = db.getCachedChannel(channelId); - final boolean needsRefresh; - if (channel == null || TextUtils.isEmpty(channel.getTitle())) { - needsRefresh = true; - } else if (staleAcceptable) { - needsRefresh = false; - } else { - needsRefresh = channel.getLastCheckTime() < System.currentTimeMillis() - (24 * 60 * 60 * 1000L); - } - if (needsRefresh && SkyTubeApp.isConnected(context)) { - channel = NewPipeService.get().getChannelDetails(channelId); - db.cacheChannel(channel); - } - if (channel != null) { - channel.setUserSubscribed(db.isUserSubscribedToChannel(channelId)); - } - return channel; - }) + public static Maybe getChannelInfo(@NonNull Context context, + @NonNull ChannelId channelId, + boolean staleAcceptable) { + return Maybe.fromCallable(() -> getChannelOrRefresh(context, channelId, staleAcceptable)) .observeOn(AndroidSchedulers.mainThread()) .doOnError(throwable -> { Log.e(TAG, "Error: " + throwable.getMessage(), throwable); @@ -80,7 +62,30 @@ public static Maybe getChannelInfo(@NonNull Context context, .subscribeOn(Schedulers.io()); } - public static Single> getSubscribedChannelView(@Nullable View progressBar, + /** + * Returns the cached information about the channel, or tries to retrieve it from the network. + */ + public static PersistentChannel getChannelOrRefresh(Context context, ChannelId channelId, boolean staleAcceptable) throws NewPipeException { + SkyTubeApp.nonUiThread(); + + final SubscriptionsDb db = SubscriptionsDb.getSubscriptionsDb(); + PersistentChannel persistentChannel = db.getCachedChannel(channelId); + final boolean needsRefresh; + if (persistentChannel == null || TextUtils.isEmpty(persistentChannel.channel().getTitle())) { + needsRefresh = true; + } else if (staleAcceptable) { + needsRefresh = false; + } else { + needsRefresh = persistentChannel.channel().getLastCheckTime() < System.currentTimeMillis() - (24 * 60 * 60 * 1000L); + } + if (needsRefresh && SkyTubeApp.isConnected(context)) { + YouTubeChannel freshChannel = NewPipeService.get().getChannelDetails(channelId); + return db.cacheChannel(persistentChannel, freshChannel); + } + return persistentChannel; + } + + public static Single> getSubscribedChannelView(Context context, @Nullable View progressBar, @Nullable String searchText) { final boolean sortChannelsAlphabetically = SkyTubeApp.getPreferenceManager() .getBoolean(SkyTubeApp.getStr(R.string.pref_key_subscriptions_alphabetical_order), false); @@ -97,6 +102,11 @@ public static Single> getSubscribedChannelView(@Nullable View if (progressBar != null) { progressBar.setVisibility(View.INVISIBLE); } + }).onErrorReturn(error -> { + Log.e(TAG, "Error: " + error.getMessage(), error); + String msg = context.getString(R.string.could_not_get_channel_detailed, error.getMessage()); + Toast.makeText(context, msg, Toast.LENGTH_LONG).show(); + return Collections.emptyList(); }); } @@ -146,25 +156,25 @@ public static Disposable isVideoWatched(@NonNull String videoId, @NonNull Menu m * @param subscribeToChannel Whether the channel should be subscribed to. * @param subscribeButton The subscribe button that the user has just clicked. * @param context The context to be used to show the toast, if necessary. - * @param channel The channel the user wants to subscribe / unsubscribe. + * @param channelId The channel id the user wants to subscribe / unsubscribe. * @param displayToastMessage Whether or not a toast should be shown. */ - public static Single subscribeToChannel(boolean subscribeToChannel, + public static Single> subscribeToChannel(boolean subscribeToChannel, @Nullable ChannelSubscriber subscribeButton, @NonNull Context context, - @NonNull YouTubeChannel channel, + @NonNull ChannelId channelId, boolean displayToastMessage) { return Single.fromCallable(() -> { - if (subscribeToChannel) { - return SubscriptionsDb.getSubscriptionsDb().subscribe(channel); - } else { - return SubscriptionsDb.getSubscriptionsDb().unsubscribe(channel.getChannelId()); - } + PersistentChannel channel = DatabaseTasks.getChannelOrRefresh(context, channelId, true); + SubscriptionsDb db = SubscriptionsDb.getSubscriptionsDb(); + DatabaseResult result = subscribeToChannel ? db.subscribe(channel, Collections.emptyList()) : db.unsubscribe(channel); + return Pair.create(channel, result); }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .doOnSuccess(databaseResult -> { - if (databaseResult == DatabaseResult.SUCCESS) { + .doOnSuccess(databaseResultPair -> { + YouTubeChannel channel = databaseResultPair.first.channel(); + if (databaseResultPair.second == DatabaseResult.SUCCESS) { // we need to refresh the Feed tab so it shows videos from the newly subscribed (or // unsubscribed) channels SkyTubeApp.getSettings().setRefreshSubsFeedFromCache(true); @@ -196,7 +206,7 @@ public static Single subscribeToChannel(boolean subscribeToChann Toast.makeText(context, R.string.unsubscribed, Toast.LENGTH_LONG).show(); } } - } else if (databaseResult == DatabaseResult.NOT_MODIFIED) { + } else if (databaseResultPair.second == DatabaseResult.NOT_MODIFIED) { if (subscribeToChannel) { Toast.makeText(context, R.string.channel_already_subscribed, Toast.LENGTH_LONG).show(); } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java index 1abbc1034d..fe21cc93fe 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java @@ -17,6 +17,12 @@ package free.rm.skytube.businessobjects.db; +import android.database.sqlite.SQLiteDatabase; + +import com.google.common.base.Joiner; + +import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; + public class LocalChannelTable { public static final String TABLE_NAME = "Channel"; public static final String COL_CHANNEL_ID = "Channel_Id"; @@ -27,6 +33,9 @@ public class LocalChannelTable { public static final String COL_THUMBNAIL_NORMAL_URL = "Thumbnail_Normal_Url"; public static final String COL_BANNER_URL = "Banner_Url"; public static final String COL_SUBSCRIBER_COUNT = "Subscriber_Count"; + public static final Column COL_ID = new Column("_id", "integer", " primary key"); + + static final String GET_ID_AND_CHANNEL_ID = String.format("SELECT %s, %s FROM %s", LocalChannelTable.COL_ID.name, LocalChannelTable.COL_CHANNEL_ID, LocalChannelTable.TABLE_NAME); public static final String[] ALL_COLUMNS = new String[]{ LocalChannelTable.COL_CHANNEL_ID, @@ -38,8 +47,9 @@ public class LocalChannelTable { LocalChannelTable.COL_LAST_VIDEO_TS, LocalChannelTable.COL_LAST_CHECK_TS}; - public static String getCreateStatement() { + public static String getCreateStatement(boolean withPk) { return "CREATE TABLE " + TABLE_NAME + " (" + + (withPk ? COL_ID.format() + "," : "") + COL_CHANNEL_ID + " TEXT UNIQUE NOT NULL, " + COL_TITLE + " TEXT, " + COL_DESCRIPTION + " TEXT, " + @@ -51,4 +61,16 @@ public static String getCreateStatement() { " )"; } + public static final void addIdColumn(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE_NAME + " RENAME TO old_" + TABLE_NAME); + db.execSQL(getCreateStatement(true)); + String allRowNames = Joiner.on(',').join(ALL_COLUMNS); + db.execSQL("insert into " + TABLE_NAME + " (_id," + allRowNames + ") select rowid," + allRowNames + " from old_" + TABLE_NAME); + db.execSQL("DROP TABLE old_" + TABLE_NAME); + } + + public static void updateLatestVideoTimestamp(SQLiteDatabase db, PersistentChannel persistentChannel, long latestPublishTimestamp) { + db.execSQL("update " + TABLE_NAME + " set " + COL_LAST_VIDEO_TS + " = max(?, "+COL_LAST_VIDEO_TS+") where " + COL_ID.name + " = ?", new Object[] { + latestPublishTimestamp, persistentChannel.subscriptionPk() }); + } } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java index 62ecf29be1..25b7b295a6 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java @@ -113,6 +113,15 @@ public static Integer executeQueryForInteger(SQLiteDatabase db, String query, In return executeQueryForInteger(db, query, null, defaultValue); } + public Long getOptionalLong(Cursor cursor, String columnName) { + int columnIndex = cursor.getColumnIndexOrThrow(columnName); + return cursor.isNull(columnIndex) ? null : cursor.getLong(columnIndex); + } + + public Long getLong(Cursor cursor, String columnName) { + return cursor.getLong(cursor.getColumnIndexOrThrow(columnName)); + } + /** * Execute the given sql updates, one-by-one. Throws an exception if any of them fails. */ diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index 770068b878..574eeac2fb 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java @@ -39,15 +39,20 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import javax.annotation.Nullable; + import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.app.Utils; import free.rm.skytube.businessobjects.Logger; import free.rm.skytube.businessobjects.YouTube.POJOs.ChannelView; +import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeVideo; import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; +import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -62,29 +67,23 @@ public class SubscriptionsDb extends SQLiteOpenHelperEx { SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, SubscriptionsVideosTable.TABLE_NAME_V2, SubscriptionsVideosTable.COL_CHANNEL_ID); private static final String FIND_EMPTY_RETRIEVAL_TS = String.format("SELECT %s,%s FROM %s WHERE %s IS NULL", SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, SubscriptionsVideosTable.COL_YOUTUBE_VIDEO, SubscriptionsVideosTable.TABLE_NAME, SubscriptionsVideosTable.COL_RETRIEVAL_TS); - private static final String sortChannelsASC = "LOWER(" + SubscriptionsTable.COL_TITLE + ") ASC "; - private static final String SUBSCRIBED_CHANNEL_INFO = String.format("SELECT %1$s,%2$s,%3$s,%4$s,(select max(%6$s) from %7$s videos where videos.%8$s = subs.%1$s) as latest_video_ts FROM %5$s subs", - SubscriptionsTable.COL_CHANNEL_ID, SubscriptionsTable.COL_TITLE, SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL, SubscriptionsTable.COL_LAST_VISIT_TIME, - SubscriptionsTable.TABLE_NAME, - SubscriptionsVideosTable.COL_PUBLISH_TIME.name, SubscriptionsVideosTable.TABLE_NAME_V2, SubscriptionsVideosTable.COL_CHANNEL_ID); - private static final String SUBSCRIBED_CHANNEL_INFO_ORDER_BY = " ORDER BY "+sortChannelsASC; - private static final String SUBSCRIBED_CHANNEL_LIMIT_BY_TITLE = " WHERE LOWER(" +SubscriptionsTable.COL_TITLE + ") like ?"; + private static final String SUBSCRIBED_CHANNEL_INFO = "SELECT c.Channel_Id,c.Title,c.Thumbnail_Normal_Url,s.Last_Visit_Time,(select max(publish_time) from subscription_videos videos where videos.Channel_Id = s.Channel_Id) as latest_video_ts FROM Subs s,Channel c where s.Channel_Id = c.Channel_Id "; + private static final String SUBSCRIBED_CHANNEL_INFO_ORDER_BY = " ORDER BY LOWER(" + LocalChannelTable.COL_TITLE + ") ASC"; + private static final String SUBSCRIBED_CHANNEL_LIMIT_BY_TITLE = " and LOWER(c." +LocalChannelTable.COL_TITLE + ") like ?"; + private static final String GET_ALL_SUBSCRIBED_CHANNEL_ID = "SELECT "+SubscriptionsTable.COL_CHANNEL_ID + " FROM "+SubscriptionsTable.TABLE_NAME; private static final String IS_SUBSCRIBED_QUERY = String.format("SELECT EXISTS(SELECT %s FROM %s WHERE %s =?) AS VAL ", SubscriptionsTable.COL_ID, SubscriptionsTable.TABLE_NAME, SubscriptionsTable.COL_CHANNEL_ID); - private static volatile SubscriptionsDb subscriptionsDb = null; - private static final int DATABASE_VERSION = 11; + private static volatile SubscriptionsDb subscriptionsDb = null; - private static final String DATABASE_NAME = "subs.db"; + private static final int DATABASE_VERSION = 15; - private Gson gson; - - private SubscriptionsDb(Context context) { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - this.gson = createGson(); - } + private static final String DATABASE_NAME = "subs.db"; + private SubscriptionsDb(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } public static synchronized SubscriptionsDb getSubscriptionsDb() { if (subscriptionsDb == null) { @@ -98,7 +97,7 @@ public static synchronized SubscriptionsDb getSubscriptionsDb() { public void onCreate(SQLiteDatabase db) { db.execSQL(SubscriptionsTable.getCreateStatement()); SubscriptionsVideosTable.addNewFlatTable(db); - db.execSQL(LocalChannelTable.getCreateStatement()); + db.execSQL(LocalChannelTable.getCreateStatement(true)); db.execSQL(CategoriesTable.getCreateStatement()); new CategoryManagement(db).setupDefaultCategories(); } @@ -119,7 +118,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { } if (upgrade.executeStep(5)) { execSQLUpdates(db, SubscriptionsTable.getLastCheckTimeColumn()); - db.execSQL(LocalChannelTable.getCreateStatement()); + db.execSQL(LocalChannelTable.getCreateStatement(false)); try { for (YouTubeChannel channel : getSubscribedChannels(db)) { if (!Utils.isEmpty(channel.getId()) && @@ -127,7 +126,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { !Utils.isEmpty(channel.getBannerUrl()) && !Utils.isEmpty(channel.getThumbnailUrl()) && !Utils.isEmpty(channel.getDescription())) { - cacheChannel(db, channel); + cacheChannel(db, null, channel); } } } catch (IOException ex) { @@ -152,6 +151,52 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (upgrade.executeStep(11)) { dropOldSubsVideosTable(db); } + if (upgrade.executeStep(12)) { + normalizeSubscriptionVideosTable(db); + } + if (upgrade.executeStep(13)) { + LocalChannelTable.addIdColumn(db); + } + if (upgrade.executeStep(14)) { + normalizeSubscriptionVideosTableSecondStep(db); + } + if (upgrade.executeStep(15)) { + SubscriptionsTable.cleanupTable(db); + } + } + + private void normalizeSubscriptionVideosTable(final SQLiteDatabase db) { + Logger.w(this, "Normalizing subscription_videos table"); + SubscriptionsVideosTable.addSubsIdColumn(db); + Map channelIdLongMap = getChannelIdLongMap(db, SubscriptionsTable.GET_ID_AND_CHANNEL_ID); + for (Map.Entry entry : channelIdLongMap.entrySet()) { + db.execSQL("update subscription_videos set subs_id = ? where Channel_Id = ?",new Object[] { + entry.getValue(), entry.getKey().getRawId() }); + } + } + + private void normalizeSubscriptionVideosTableSecondStep(final SQLiteDatabase db) { + Logger.w(this, "Normalizing subscription_videos table - 2nd step"); + SubscriptionsVideosTable.addChannelPkColumn(db); + Map channelIdLongMap = getChannelIdLongMap(db, LocalChannelTable.GET_ID_AND_CHANNEL_ID); + for (Map.Entry entry : channelIdLongMap.entrySet()) { + db.execSQL("update subscription_videos set channel_pk = ? where Channel_Id = ?",new Object[] { + entry.getValue(), entry.getKey().getRawId() }); + } + } + + private static Map getChannelIdLongMap(final SQLiteDatabase db, final String query) { + Map channelIdLongMap = new HashMap<>(); + try (Cursor cursor = db.rawQuery(query, null)) { + final int _idIdx = cursor.getColumnIndex(SubscriptionsTable.COL_ID); + final int channelIdx = cursor.getColumnIndex(SubscriptionsTable.COL_CHANNEL_ID); + while (cursor.moveToNext()) { + Long id = Objects.requireNonNull(cursor.getLong(_idIdx), "missing _id column"); + String channelIds = Objects.requireNonNull(cursor.getString(channelIdx), "missing channelIdx column"); + channelIdLongMap.put(new ChannelId(channelIds), id); + } + } + return channelIdLongMap; } private void dropOldSubsVideosTable(final SQLiteDatabase db) { @@ -166,8 +211,17 @@ private void fixChannelIds(final SQLiteDatabase db) { private void migrateFromJsonColumn(final SQLiteDatabase db) { int fullCount = 0; + Gson gson = new GsonBuilder().registerTypeAdapter(YouTubeChannel.class, (JsonSerializer) (src, typeOfSrc, context) -> { + JsonObject obj = new JsonObject(); + obj.addProperty("id", src.getId()); + obj.addProperty("title", src.getTitle()); + obj.addProperty("description", src.getDescription()); + obj.addProperty("thumbnailNormalUrl", src.getThumbnailUrl()); + obj.addProperty("bannerUrl", src.getBannerUrl()); + return obj; + }).create(); while (true) { - int success = migrateFromJsonColumnBlock(db); + int success = migrateFromJsonColumnBlock(db, gson); if (success > 0) { fullCount += success; } else { @@ -177,7 +231,7 @@ private void migrateFromJsonColumn(final SQLiteDatabase db) { } } - private int migrateFromJsonColumnBlock(final SQLiteDatabase db) { + private int migrateFromJsonColumnBlock(final SQLiteDatabase db, Gson gson) { int counter = 0; int success = 0; try (Cursor cursor = db.query(SubscriptionsVideosTable.TABLE_NAME, @@ -227,48 +281,50 @@ private void setupRetrievalTimestamp(SQLiteDatabase db) { /** * Saves the given channel into the subscriptions DB. * - * @param channel Channel the user wants to subscribe to. - * + * @param persistentChannel Channel the user wants to subscribe to. + * @param videos the channel videos, which needs to be added to the database. + * * @return DatabaseResult.SUCCESS if the operation was successful; NOT_MODIFIED or ERROR otherwise. */ - public DatabaseResult subscribe(YouTubeChannel channel) { + public DatabaseResult subscribe(PersistentChannel persistentChannel, Collection videos) { SkyTubeApp.nonUiThread(); - saveChannelVideos(channel.getYouTubeVideos(), channel.getChannelId()); + saveChannelVideos(videos, persistentChannel, false); - return saveSubscription(channel); + return saveSubscription(persistentChannel.channel().getChannelId()); } /** * Saves the given channel into the subscriptions DB. * - * @param channel The channel the user wants to subscribe to. + * @param channelId The id of the channel the user wants to subscribe to. * * @return True if the operation was successful; false otherwise. */ - private DatabaseResult saveSubscription(YouTubeChannel channel) { + private DatabaseResult saveSubscription(ChannelId channelId) { SkyTubeApp.nonUiThread(); ContentValues values = new ContentValues(); - values.put(SubscriptionsTable.COL_CHANNEL_ID, channel.getId()); - values.put(SubscriptionsTable.COL_LAST_VISIT_TIME, channel.getLastVisitTime()); - values.put(SubscriptionsTable.COL_TITLE, channel.getTitle()); - values.put(SubscriptionsTable.COL_DESCRIPTION, channel.getDescription()); - values.put(SubscriptionsTable.COL_BANNER_URL, channel.getBannerUrl()); - values.put(SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL, channel.getThumbnailUrl()); - values.put(SubscriptionsTable.COL_CATEGORY_ID.name, channel.getCategoryId()); - values.put(SubscriptionsTable.COL_SUBSCRIBER_COUNT, channel.getSubscriberCount()); + values.put(SubscriptionsTable.COL_CHANNEL_ID, channelId.getRawId()); +// values.put(SubscriptionsTable.COL_LAST_VISIT_TIME, channel.getLastVisitTime()); +// values.put(SubscriptionsTable.COL_TITLE, channel.getTitle()); +// values.put(SubscriptionsTable.COL_DESCRIPTION, channel.getDescription()); +// values.put(SubscriptionsTable.COL_BANNER_URL, channel.getBannerUrl()); +// values.put(SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL, channel.getThumbnailUrl()); +// values.put(SubscriptionsTable.COL_CATEGORY_ID.name, channel.getCategoryId()); +// values.put(SubscriptionsTable.COL_SUBSCRIBER_COUNT, channel.getSubscriberCount()); SQLiteDatabase db = getWritableDatabase(); try { long result = db.insertWithOnConflict(SubscriptionsTable.TABLE_NAME, null, values, SQLiteDatabase.CONFLICT_IGNORE); if (result > 0) { + return DatabaseResult.SUCCESS; } - if (isUserSubscribedToChannel(channel.getChannelId())) { - Logger.i(this, "Already subscribed to " + channel.getId()); + if (isUserSubscribedToChannel(channelId)) { + Logger.i(this, "Already subscribed to " + channelId); return DatabaseResult.NOT_MODIFIED; } - Logger.i(this, "Unable to subscribe to " + channel.getId()); + Logger.i(this, "Unable to subscribe to " + channelId); return DatabaseResult.ERROR; } catch (SQLException e) { Logger.e(this, "Error during subscribing: " + e.getMessage(), e); @@ -279,22 +335,22 @@ private DatabaseResult saveSubscription(YouTubeChannel channel) { /** * Removes the given channel from the subscriptions DB. * - * @param channelId id of the channel the user wants to unsubscribe to. + * @param channel the channel the user wants to unsubscribe from. * * @return True if the operation was successful; false otherwise. */ - public DatabaseResult unsubscribe(ChannelId channelId) { + public DatabaseResult unsubscribe(PersistentChannel channel) { SkyTubeApp.nonUiThread(); // delete any feed videos pertaining to this channel getWritableDatabase().delete(SubscriptionsVideosTable.TABLE_NAME_V2, - SubscriptionsVideosTable.COL_CHANNEL_ID + " = ?", - new String[]{channelId.getRawId()}); + SubscriptionsVideosTable.COL_SUBS_ID.name + " = ?", + new String[]{String.valueOf(channel.channelPk())}); // remove this channel from the subscriptions DB int rowsDeleted = getWritableDatabase().delete(SubscriptionsTable.TABLE_NAME, SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channelId.getRawId()}); + new String[]{channel.channel().getId()}); // Need to make sure when we come back to MainActivity, that we refresh the Feed tab so it hides videos from the newly unsubscribed SkyTubeApp.getSettings().setRefreshSubsFeedFromCache(true); @@ -346,10 +402,15 @@ public void updateVideo(YouTubeVideo video) { new String[] { video.getId() }); } - private ContentValues convertToContentValues(final YouTubeVideo video, ChannelId channelId) { + private ContentValues convertToContentValues(final YouTubeVideo video, @Nullable PersistentChannel persistentChannel) { ContentValues values = new ContentValues(); - ChannelId chId = channelId != null ? channelId : video.getChannelId(); - values.put(SubscriptionsVideosTable.COL_CHANNEL_ID_V2.name, chId.getRawId()); + if (persistentChannel != null) { + ChannelId chId = persistentChannel.channel().getChannelId(); + values.put(SubscriptionsVideosTable.COL_CHANNEL_ID_V2.name, chId.getRawId()); + values.put(SubscriptionsVideosTable.COL_SUBS_ID.name, persistentChannel.subscriptionPk()); + values.put(SubscriptionsVideosTable.COL_CHANNEL_PK.name, persistentChannel.channelPk()); + } + values.put(SubscriptionsVideosTable.COL_CHANNEL_TITLE.name, video.getSafeChannelName()); values.put(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID_V2.name, video.getId()); values.put(SubscriptionsVideosTable.COL_CATEGORY_ID.name, video.getCategoryId()); @@ -389,7 +450,7 @@ public int setPublishTimestamp(YouTubeVideo video) { private List getSubscribedChannelIds() { SkyTubeApp.nonUiThread(); - try (Cursor cursor = getReadableDatabase().rawQuery("SELECT "+SubscriptionsTable.COL_CHANNEL_ID + " FROM "+SubscriptionsTable.TABLE_NAME,null)) { + try (Cursor cursor = getReadableDatabase().rawQuery(GET_ALL_SUBSCRIBED_CHANNEL_ID,null)) { List result = new ArrayList<>(); while(cursor.moveToNext()) { result.add(new ChannelId(cursor.getString(0))); @@ -413,23 +474,19 @@ public Single> getSubscribedChannelIdsAsync() { private List getSubscribedChannels(SQLiteDatabase db) throws IOException { SkyTubeApp.nonUiThread(); - try (Cursor cursor = db.query(SubscriptionsTable.TABLE_NAME, - SubscriptionsTable.ALL_COLUMNS, - null, null, - null, null, - SubscriptionsTable.COL_ID + " ASC")) { + try (Cursor cursor = db.rawQuery("select s._id subs_id, s.category_id, s.Last_Visit_Time, c.* from Subs s join Channel c on s.Channel_Id = c.Channel_Id", null)) { List subsChannels = new ArrayList<>(); if (cursor.moveToNext()) { - final int colChannelIdNum = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CHANNEL_ID); - final int colTitle = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_TITLE); - final int colDescription = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_DESCRIPTION); - final int colBanner = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_BANNER_URL); - final int colThumbnail = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL); - final int colSubscribers = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_SUBSCRIBER_COUNT); - final int colLastVisit = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_LAST_VISIT_TIME); - final int colLastCheck = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_LAST_CHECK_TIME); + final int colChannelIdNum = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_CHANNEL_ID); + final int colTitle = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_TITLE); + final int colDescription = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_DESCRIPTION); + final int colBanner = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_BANNER_URL); + final int colThumbnail = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_THUMBNAIL_NORMAL_URL); + final int colSubscribers = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_SUBSCRIBER_COUNT); + final int colLastCheck = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_LAST_CHECK_TS); + final int colLastVisit = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_LAST_VISIT_TIME); final int colCategoryId = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CATEGORY_ID.name); do { @@ -450,44 +507,6 @@ private Integer getInteger(Cursor cursor, int colCategoryId) { return (colCategoryId < 0 || cursor.isNull(colCategoryId)) ? null : cursor.getInt(colCategoryId); } - public YouTubeChannel getCachedSubscribedChannel(ChannelId channelId) { - SkyTubeApp.nonUiThread(); - - try (Cursor cursor = getReadableDatabase().query(SubscriptionsTable.TABLE_NAME, - SubscriptionsTable.ALL_COLUMNS, - SubscriptionsTable.COL_CHANNEL_ID + " = ?", new String[]{channelId.getRawId()}, - null, null, null)) { - - YouTubeChannel channel = null; - - if (cursor.moveToNext()) { - final int colChannelIdNum = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CHANNEL_ID); - final int colTitle = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_TITLE); - final int colDescription = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_DESCRIPTION); - final int colBanner = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_BANNER_URL); - final int colThumbnail = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL); - final int colSubscribers = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_SUBSCRIBER_COUNT); - final int colLastVisit = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_LAST_VISIT_TIME); - final int colLastCheck = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_LAST_CHECK_TIME); - final int colCategoryId = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CATEGORY_ID.name); - - final String id = cursor.getString(colChannelIdNum); - Integer categoryId = getInteger(cursor, colCategoryId); - - channel = new YouTubeChannel(id, cursor.getString(colTitle), - cursor.getString(colDescription), cursor.getString(colThumbnail), - cursor.getString(colBanner), cursor.getLong(colSubscribers), true, - cursor.getLong(colLastVisit), cursor.getLong(colLastCheck), - categoryId, Collections.emptyList()); - - } - - return channel; - } - } - - - /** * Checks if the user is subscribed to the given channel. * @@ -538,102 +557,69 @@ public Single updateLastVisitTimeAsync(String channelId) { * * @return last visit time, if the update was successful; -1 otherwise. */ - public long updateLastCheckTime(String channelId) { + public long updateLastVideoFetch(ChannelId channelId) { SQLiteDatabase db = getWritableDatabase(); long currentTime = System.currentTimeMillis(); ContentValues values = new ContentValues(); - values.put(SubscriptionsTable.COL_LAST_CHECK_TIME, currentTime); + values.put(SubscriptionsTable.COL_LAST_VIDEO_FETCH, currentTime); int count = db.update( SubscriptionsTable.TABLE_NAME, values, SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channelId}); + new String[]{channelId.getRawId()}); return (count > 0 ? currentTime : -1); } - /** - * Update channel informations in the database from the Object. - * - * @param channel which contains all the recent informations. - * @return true, if the channel was inside the database. - */ - public boolean updateChannel(YouTubeChannel channel) { - SQLiteDatabase db = getWritableDatabase(); - - ContentValues values = new ContentValues(); - values.put(SubscriptionsTable.COL_TITLE, channel.getTitle()); - values.put(SubscriptionsTable.COL_DESCRIPTION, channel.getDescription()); - values.put(SubscriptionsTable.COL_BANNER_URL, channel.getBannerUrl()); - values.put(SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL, channel.getThumbnailUrl()); - values.put(SubscriptionsTable.COL_SUBSCRIBER_COUNT, channel.getSubscriberCount()); - values.put(SubscriptionsTable.COL_LAST_VISIT_TIME, channel.getLastVisitTime()); - values.put(SubscriptionsTable.COL_CATEGORY_ID.name, channel.getCategoryId()); - - int count = db.update( - SubscriptionsTable.TABLE_NAME, - values, - SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channel.getId()}); - return count > 0; - } - private boolean hasVideo(YouTubeVideo video) { return executeQueryForInteger(HAS_VIDEO_QUERY, new String[]{video.getId()}, 0) > 0; } - /** - * Loop through each video saved in the passed {@link YouTubeChannel} and save it into the database, if it's not already been saved - * @param videos the list of videos - * @param channelId the channel id - */ - public void saveChannelVideos(Collection videos, ChannelId channelId) { - for (YouTubeVideo video : videos) { - if(video.getPublishTimestamp() != null && !hasVideo(video)) { - ContentValues values = createContentValues(video, channelId); - getWritableDatabase().insert(SubscriptionsVideosTable.TABLE_NAME_V2, null, values); - } - } - } - /** * Loop through each video saved in the passed {@link YouTubeChannel} and insert it into the database, or update it. * @param videos the list of videos - * @param channelId the channel id + * @param persistentChannel information about the persisted channel. */ - public void saveVideos(List videos, ChannelId channelId) { - SkyTubeApp.nonUiThread(); - SQLiteDatabase db = getWritableDatabase(); - for (YouTubeVideo video : videos) { - if (video.getPublishTimestamp() != null) { - ContentValues values = createContentValues(video, channelId); - if (hasVideo(video)) { - values.remove(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID); - db.update(SubscriptionsVideosTable.TABLE_NAME_V2, - values, - SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID_EQUALS_TO, - new String[]{video.getId()}); - } else { - db.insert(SubscriptionsVideosTable.TABLE_NAME_V2, null, values); - } - } - } - } + public void saveChannelVideos(Collection videos, PersistentChannel persistentChannel, boolean doUpdate) { + SkyTubeApp.nonUiThread(); + SQLiteDatabase db = getWritableDatabase(); + long latestPublishTimestamp = 0; + + for (YouTubeVideo video : videos) { + if (video.getPublishTimestamp() != null) { + latestPublishTimestamp = Math.max(latestPublishTimestamp, video.getPublishTimestamp()); + ContentValues values = createContentValues(video, persistentChannel); + if (hasVideo(video)) { + if (doUpdate) { + values.remove(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID); + db.update(SubscriptionsVideosTable.TABLE_NAME_V2, + values, + SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID_EQUALS_TO, + new String[]{video.getId()}); + } + } else { + db.insert(SubscriptionsVideosTable.TABLE_NAME_V2, null, values); + } + } + } + SubscriptionsTable.updateLastVideoFetchTimestamps(db, persistentChannel); + LocalChannelTable.updateLatestVideoTimestamp(db, persistentChannel, latestPublishTimestamp); + } /** * Insert videos into the subscription video table. * @param videos */ - public void insertVideosForChannel(List videos, ChannelId channelId) { + public void insertVideosForChannel(List videos, PersistentChannel channel) { SkyTubeApp.nonUiThread(); SQLiteDatabase db = getWritableDatabase(); for (YouTubeVideo video : videos) { try { if (video.getPublishTimestamp() != null) { - ContentValues values = createContentValues(video, channelId); + ContentValues values = createContentValues(video, channel); db.insert(SubscriptionsVideosTable.TABLE_NAME_V2, null, values); } } catch (Exception e) { @@ -642,8 +628,8 @@ public void insertVideosForChannel(List videos, ChannelId channelI } } - private ContentValues createContentValues(YouTubeVideo video, ChannelId channelId) { - ContentValues values = convertToContentValues(video, channelId); + private ContentValues createContentValues(YouTubeVideo video, PersistentChannel persistentChannel) { + ContentValues values = convertToContentValues(video, persistentChannel); values.put(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, video.getId()); final long publishInstant = video.getPublishTimestamp(); values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name, publishInstant); @@ -679,18 +665,6 @@ public List getSubscriptionVideoPage(int limit, String videoId, lo return extractVideos(cursor, true); } - private Gson createGson() { - return new GsonBuilder().registerTypeAdapter(YouTubeChannel.class, (JsonSerializer) (src, typeOfSrc, context) -> { - JsonObject obj = new JsonObject(); - obj.addProperty("id", src.getId()); - obj.addProperty("title", src.getTitle()); - obj.addProperty("description", src.getDescription()); - obj.addProperty("thumbnailNormalUrl", src.getThumbnailUrl()); - obj.addProperty("bannerUrl", src.getBannerUrl()); - return obj; - }).create(); - } - /** * Load YouTubeVideo objects from a cursor, only SubscriptionsVideosTable.COL_YOUTUBE_VIDEO column is needed. * @param cursor the cursor to process @@ -747,83 +721,69 @@ private List extractVideos(Cursor cursor, boolean fullColumnList) } if (!invalidIds.isEmpty()) { Logger.e(this, "Found videos without channel: {}", invalidIds); -// deleteVideosByIds(invalidIds); } return videos; } - private void deleteVideosByIds(Set ids) { - for (String id: ids) { - Logger.w(this, "delete video by id: "+ id); - int rowsDeleted = getWritableDatabase().delete(SubscriptionsVideosTable.TABLE_NAME_V2, - SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID + " = ?", - new String[]{id}); - Logger.w(this, "result "+rowsDeleted+" deleted"); - } + public Maybe getChannel(ChannelId channelId) { + return Maybe.fromCallable(() -> getCachedChannel(channelId)); } - // Generic channel caching - /** * * @param channelId * @return all the information stored in the local cache about the channel. */ - public YouTubeChannel getCachedChannel(ChannelId channelId) { - try (Cursor cursor = getReadableDatabase().query(LocalChannelTable.TABLE_NAME, - LocalChannelTable.ALL_COLUMNS, - LocalChannelTable.COL_CHANNEL_ID + " = ?", new String[] { channelId.getRawId() }, - null, null, null)) { + public PersistentChannel getCachedChannel(ChannelId channelId) { + SkyTubeApp.nonUiThread(); + try (Cursor cursor = getReadableDatabase().rawQuery( + "select s._id subs_id, c.* from Channel c left outer Join Subs s on c.Channel_Id = s.Channel_Id where c.Channel_Id = ?", + new String[] { channelId.getRawId() })) { if (cursor.moveToNext()) { + Long subscriptionPk = getOptionalLong(cursor, "subs_id"); + Long channelPk = getLong(cursor, LocalChannelTable.COL_ID.name); + String title = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_TITLE)); String description = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_DESCRIPTION)); String thumbnail = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_THUMBNAIL_NORMAL_URL)); String banner = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_BANNER_URL)); - long subscriberCount = cursor.getLong(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_SUBSCRIBER_COUNT)); - long lastCheckTs = cursor.getLong(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_LAST_CHECK_TS)); - return new YouTubeChannel(channelId.getRawId(), title, description, thumbnail, banner, subscriberCount, false, -1, lastCheckTs, null, Collections.emptyList()); - } + long subscriberCount = getLong(cursor, LocalChannelTable.COL_SUBSCRIBER_COUNT); + long lastCheckTs = getLong(cursor, LocalChannelTable.COL_LAST_CHECK_TS); + // TODO: use + Long lastVideoTs = getOptionalLong(cursor, LocalChannelTable.COL_LAST_VIDEO_TS); + YouTubeChannel channel = new YouTubeChannel(channelId.getRawId(), title, description, thumbnail, banner, subscriberCount, subscriptionPk != null, -1, lastCheckTs, null, Collections.emptyList()); + return new PersistentChannel(channel, channelPk, subscriptionPk); + } } return null; } - /** - * Save channel informations in the database from the Object. - * - * @param channel which contains all the recent informations. - * @return true, if the channel was inside the database. - */ - public boolean cacheChannel(YouTubeChannel channel) { - SQLiteDatabase db = getWritableDatabase(); - updateSubscribedChannelTable(db, channel); - return cacheChannel(db, channel); - } - - private boolean updateSubscribedChannelTable(SQLiteDatabase db, YouTubeChannel channel) { - ContentValues values = new ContentValues(); - values.put(SubscriptionsTable.COL_TITLE, channel.getTitle()); - values.put(SubscriptionsTable.COL_DESCRIPTION, channel.getDescription()); - values.put(SubscriptionsTable.COL_BANNER_URL, channel.getBannerUrl()); - values.put(SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL, channel.getThumbnailUrl()); - values.put(SubscriptionsTable.COL_SUBSCRIBER_COUNT, channel.getSubscriberCount()); - values.put(SubscriptionsTable.COL_LAST_CHECK_TIME, channel.getLastCheckTime()); - values.put(SubscriptionsTable.COL_CATEGORY_ID.name, channel.getCategoryId()); - - int count = db.update( - SubscriptionsTable.TABLE_NAME, - values, - SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channel.getId()}); - return count > 0; - } + /** + * Save channel informations in the database from the Object. + * + * @param channel which contains all the recent informations. + * @return true, if the channel was inside the database. + */ + public PersistentChannel cacheChannel(@Nullable PersistentChannel persistentChannel, YouTubeChannel channel) { + SQLiteDatabase db = getWritableDatabase(); + return cacheChannel(db, persistentChannel, channel); + } - private boolean cacheChannel(SQLiteDatabase db, YouTubeChannel channel) { - ContentValues values = new ContentValues(); - values.put(LocalChannelTable.COL_TITLE, channel.getTitle()); - values.put(LocalChannelTable.COL_DESCRIPTION, channel.getDescription()); - values.put(LocalChannelTable.COL_BANNER_URL, channel.getBannerUrl()); - values.put(LocalChannelTable.COL_THUMBNAIL_NORMAL_URL, channel.getThumbnailUrl()); - values.put(LocalChannelTable.COL_SUBSCRIBER_COUNT, channel.getSubscriberCount()); + private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentChannel persistentChannel, YouTubeChannel channel) { + ContentValues values = new ContentValues(); + values.put(LocalChannelTable.COL_TITLE, channel.getTitle()); + if (!Utils.isEmpty(channel.getDescription())) { + values.put(LocalChannelTable.COL_DESCRIPTION, channel.getDescription()); + } + if (!Utils.isEmpty(channel.getBannerUrl())) { + values.put(LocalChannelTable.COL_BANNER_URL, channel.getBannerUrl()); + } + if (!Utils.isEmpty(channel.getThumbnailUrl())) { + values.put(LocalChannelTable.COL_THUMBNAIL_NORMAL_URL, channel.getThumbnailUrl()); + } + if (channel.getSubscriberCount() > 0) { + values.put(LocalChannelTable.COL_SUBSCRIBER_COUNT, channel.getSubscriberCount()); + } if(channel.getLastVideoTime() > 0) { values.put(LocalChannelTable.COL_LAST_VIDEO_TS, channel.getLastVideoTime()); } @@ -831,41 +791,29 @@ private boolean cacheChannel(SQLiteDatabase db, YouTubeChannel channel) { values.put(LocalChannelTable.COL_LAST_CHECK_TS, channel.getLastCheckTime()); } - int count = db.update( - LocalChannelTable.TABLE_NAME, - values, - LocalChannelTable.COL_CHANNEL_ID + " = ?", - new String[]{channel.getId()}); - if (count > 0) { - return true; - } else if (count == 0) { - values.put(LocalChannelTable.COL_CHANNEL_ID, channel.getId()); - return db.insert(LocalChannelTable.TABLE_NAME, null, values) > 0; - } - return false; - } - - /** - * Removes the given channel from the local channel cache. - * - * @param channelId id of the channel. - * - * @return True if the operation was successful; false otherwise. - */ - public boolean removeCachedChannel(String channelId) { - // remove this channel from the subscriptions DB - int rowsDeleted = getWritableDatabase().delete(LocalChannelTable.TABLE_NAME, - LocalChannelTable.COL_CHANNEL_ID + " = ?", - new String[]{channelId}); - return (rowsDeleted >= 0); - } + // If there is a persistentChannel info, we already have the channel in the db + if (persistentChannel != null) { + // Try to update it ... + int count = db.update( + LocalChannelTable.TABLE_NAME, + values, + LocalChannelTable.COL_ID.name + " = ?", + new String[]{ String.valueOf(persistentChannel.channelPk())}); + if (count > 0) { + return persistentChannel.with(channel); + } + } + values.put(LocalChannelTable.COL_CHANNEL_ID, channel.getId()); + long newPk = db.insert(LocalChannelTable.TABLE_NAME, null, values); + return new PersistentChannel(channel, newPk, null); + } public List getSubscribedChannelsByText(String searchText, boolean sortChannelsAlphabetically) { List result = new ArrayList<>(); try (Cursor cursor = createSubscriptionCursor(searchText, sortChannelsAlphabetically)) { final int channelId = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CHANNEL_ID); - final int title = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_TITLE); - final int thumbnail = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL); + final int title = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_TITLE); + final int thumbnail = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_THUMBNAIL_NORMAL_URL); final int colLastVisit = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_LAST_VISIT_TIME); final int colLatestVideoTs = cursor.getColumnIndexOrThrow("latest_video_ts"); while(cursor.moveToNext()) { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java index bb7d9d4fa5..21a424b09a 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java @@ -19,65 +19,70 @@ import android.database.sqlite.SQLiteDatabase; +import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; + /** * YouTube channels subscriptions table. */ public class SubscriptionsTable { - public static final String TABLE_NAME = "Subs"; - public static final String COL_ID = "_id"; - public static final String COL_CHANNEL_ID = "Channel_Id"; - public static final String COL_LAST_VISIT_TIME = "Last_Visit_Time"; - public static final String COL_LAST_CHECK_TIME = "Last_Check_Time"; - public static final String COL_TITLE = "Title"; - public static final String COL_DESCRIPTION = "Description"; - public static final String COL_THUMBNAIL_NORMAL_URL = "Thumbnail_Normal_Url"; - public static final String COL_BANNER_URL = "Banner_Url"; - public static final String COL_SUBSCRIBER_COUNT = "Subscriber_Count"; - public static final Column COL_CATEGORY_ID = new Column("category_id", "INTEGER"); - public static final String[] ALL_COLUMNS = new String[]{ - SubscriptionsTable.COL_CHANNEL_ID, - SubscriptionsTable.COL_TITLE, - SubscriptionsTable.COL_DESCRIPTION, - SubscriptionsTable.COL_BANNER_URL, - SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL, - SubscriptionsTable.COL_SUBSCRIBER_COUNT, - SubscriptionsTable.COL_CATEGORY_ID.name, - SubscriptionsTable.COL_LAST_VISIT_TIME, - SubscriptionsTable.COL_LAST_CHECK_TIME}; + public static final String TABLE_NAME = "Subs"; + public static final String COL_ID = "_id"; + public static final String COL_CHANNEL_ID = "Channel_Id"; + public static final String COL_LAST_VISIT_TIME = "Last_Visit_Time"; + public static final String COL_LAST_CHECK_TIME = "Last_Check_Time"; + private static final String COL_TITLE = "Title"; + private static final String COL_DESCRIPTION = "Description"; + private static final String COL_THUMBNAIL_NORMAL_URL = "Thumbnail_Normal_Url"; + private static final String COL_BANNER_URL = "Banner_Url"; + private static final String COL_SUBSCRIBER_COUNT = "Subscriber_Count"; + public static final Column COL_CATEGORY_ID = new Column("category_id", "INTEGER"); + public static final String COL_LAST_VIDEO_FETCH = "last_video_fetch_time"; - private static final String ADD_COLUMN = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN "; + private static final String ADD_COLUMN = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN "; - public static String getCreateStatement() { - return "CREATE TABLE " + TABLE_NAME + " (" + - COL_ID + " INTEGER PRIMARY KEY ASC, " + - COL_CHANNEL_ID + " TEXT UNIQUE NOT NULL, " + - COL_TITLE + " TEXT, " + - COL_DESCRIPTION + " TEXT, " + - COL_THUMBNAIL_NORMAL_URL+ " TEXT, " + - COL_BANNER_URL + " TEXT, " + - COL_SUBSCRIBER_COUNT + " INTEGER, " + - COL_CATEGORY_ID.format() + ", " + - COL_LAST_VISIT_TIME + " TIMESTAMP DEFAULT (strftime('%s', 'now')), " + - COL_LAST_CHECK_TIME + " INTEGER " + - " )"; - } + static final String GET_ID_AND_CHANNEL_ID = String.format("SELECT %s, %s FROM %s", SubscriptionsTable.COL_ID, SubscriptionsTable.COL_CHANNEL_ID, SubscriptionsTable.TABLE_NAME); - public static String[] getAddColumns() { - return new String[]{ - ADD_COLUMN + COL_TITLE + " TEXT", - ADD_COLUMN + COL_DESCRIPTION + " TEXT", - ADD_COLUMN + COL_THUMBNAIL_NORMAL_URL + " TEXT", - ADD_COLUMN + COL_BANNER_URL + " TEXT", - ADD_COLUMN + COL_SUBSCRIBER_COUNT + " INTEGER" - }; - } + public static String getCreateStatement() { + return "CREATE TABLE " + TABLE_NAME + " (" + + COL_ID + " INTEGER PRIMARY KEY ASC, " + + COL_CHANNEL_ID + " TEXT UNIQUE NOT NULL, " + + COL_CATEGORY_ID.format() + ", " + + COL_LAST_VISIT_TIME + " TIMESTAMP DEFAULT (strftime('%s', 'now')), " + + COL_LAST_VIDEO_FETCH + " INTEGER " + + " )"; + } + + public static String[] getAddColumns() { + return new String[]{ + ADD_COLUMN + COL_TITLE + " TEXT", + ADD_COLUMN + COL_DESCRIPTION + " TEXT", + ADD_COLUMN + COL_THUMBNAIL_NORMAL_URL + " TEXT", + ADD_COLUMN + COL_BANNER_URL + " TEXT", + ADD_COLUMN + COL_SUBSCRIBER_COUNT + " INTEGER" + }; + } public static void addCategoryColumn(SQLiteDatabase db) { SQLiteOpenHelperEx.addColumn(db, TABLE_NAME, COL_CATEGORY_ID); } - public static String[] getLastCheckTimeColumn() { - return new String[] { ADD_COLUMN + COL_LAST_CHECK_TIME + " INTEGER "}; - } + public static String[] getLastCheckTimeColumn() { + return new String[]{ADD_COLUMN + COL_LAST_CHECK_TIME + " INTEGER "}; + } + + public static void cleanupTable(SQLiteDatabase db) { + db.execSQL("ALTER TABLE " + TABLE_NAME + " RENAME TO old_" + TABLE_NAME); + db.execSQL(getCreateStatement()); + db.execSQL("insert into " + TABLE_NAME + " (_id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_VIDEO_FETCH + + ") select _id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_CHECK_TIME + " from old_" + TABLE_NAME); + db.execSQL("DROP TABLE old_" + TABLE_NAME); + } + + public static void updateLastVideoFetchTimestamps(SQLiteDatabase db, PersistentChannel persistentChannel) { + if (persistentChannel.isSubscribed()) { + db.execSQL("update " + TABLE_NAME + " set " + COL_LAST_VIDEO_FETCH + " = ? where " + COL_ID + " = ?", new Object[] { + System.currentTimeMillis(), persistentChannel.subscriptionPk() }); + } + } } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java index 8470703c3f..e4ae7db74c 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java @@ -49,6 +49,8 @@ public class SubscriptionsVideosTable { public static final Column COL_DURATION = new Column("duration", "integer", "not null default 0"); public static final Column COL_PUBLISH_TIME = new Column("publish_time", "integer", "not null default 0"); public static final Column COL_THUMBNAIL_URL = new Column("thumbnail_url", "text"); + public static final Column COL_SUBS_ID = new Column("subs_id", "integer"); + public static final Column COL_CHANNEL_PK = new Column("channel_pk", "integer"); public static final String COL_YOUTUBE_VIDEO_ID_EQUALS_TO = SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID + " = ?"; @@ -109,6 +111,8 @@ public static String getIndexOnVideos() { public static void addNewFlatTable(SQLiteDatabase db) { SQLiteOpenHelperEx.createTable(db, TABLE_NAME_V2, + COL_CHANNEL_PK, + COL_SUBS_ID, COL_CHANNEL_ID_V2, COL_YOUTUBE_VIDEO_ID_V2, COL_CATEGORY_ID, @@ -125,4 +129,13 @@ public static void addNewFlatTable(SQLiteDatabase db) { ); SQLiteOpenHelperEx.createIndex(db, IDX_PUBLISH_TS_V2, TABLE_NAME_V2, COL_CATEGORY_ID); } + + static void addSubsIdColumn(SQLiteDatabase db) { + SQLiteOpenHelperEx.addColumn(db, TABLE_NAME_V2, COL_SUBS_ID); + } + + static void addChannelPkColumn(SQLiteDatabase db) { + SQLiteOpenHelperEx.addColumn(db, TABLE_NAME_V2, COL_CHANNEL_PK); + } + } diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/SubscriptionsBackupsManager.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/SubscriptionsBackupsManager.java index cc67f45add..95f8840d7e 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/SubscriptionsBackupsManager.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/SubscriptionsBackupsManager.java @@ -41,6 +41,7 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; @@ -50,14 +51,19 @@ import free.rm.skytube.app.EventBus; import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.businessobjects.Logger; +import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; +import free.rm.skytube.businessobjects.YouTube.YouTubeTasks; import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; import free.rm.skytube.businessobjects.YouTube.newpipe.ContentId; +import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeException; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeService; +import free.rm.skytube.businessobjects.db.DatabaseResult; import free.rm.skytube.businessobjects.db.DatabaseTasks; import free.rm.skytube.businessobjects.db.SubscriptionsDb; import free.rm.skytube.gui.businessobjects.preferences.BackupDatabases; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -493,11 +499,22 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { }).subscribeOn(AndroidSchedulers.mainThread()) .observeOn(Schedulers.io()) .map(dialog -> { - for (MultiSelectListPreferenceItem channel : channels) { - SubscriptionsDb.getSubscriptionsDb().subscribe(new YouTubeChannel(channel.id, channel.text)); + SubscriptionsDb db = SubscriptionsDb.getSubscriptionsDb(); + int success = 0; + for (MultiSelectListPreferenceItem selectedItem : channels) { + try { + ChannelId channelId = new ChannelId(selectedItem.id); + PersistentChannel channelInfo = DatabaseTasks.getChannelOrRefresh(activity, channelId, true); + if (!channelInfo.isSubscribed()) { + db.subscribe(channelInfo, Collections.emptyList()); + success += 1; + } + } catch (NewPipeException newPipeException) { + Log.e(TAG, "Error: " + newPipeException.getMessage(), newPipeException); + } } - return new Object[] { dialog, channels.size() }; + return new Object[] { dialog, success }; }) .observeOn(AndroidSchedulers.mainThread()) .map(inputs -> { diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SubsAdapter.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SubsAdapter.java index 089ad3db36..27c4170162 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SubsAdapter.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SubsAdapter.java @@ -142,7 +142,7 @@ private void refreshFilteredSubsList(String searchText) { } private void executeQuery(String searchText, View progressBar) { - compositeDisposable.add(DatabaseTasks.getSubscribedChannelView(progressBar, searchText) + compositeDisposable.add(DatabaseTasks.getSubscribedChannelView(getContext(), progressBar, searchText) .subscribe(this::appendList)); } diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java index a53552702f..939d8cb499 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java @@ -45,7 +45,7 @@ public boolean handleChannelActions(Context context, YouTubeChannel channel, int return true; case R.id.unsubscribe_channel: compositeDisposable.add(DatabaseTasks.subscribeToChannel(false, - null, context, channel, true).subscribe()); + null, context, channel.getChannelId(), true).subscribe()); return true; case R.id.open_channel: SkyTubeApp.launchChannel(channel.getChannelId(), context); diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/views/SubscribeButton.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/SubscribeButton.java index c335ad7411..f642eb0958 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/views/SubscribeButton.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/SubscribeButton.java @@ -64,7 +64,7 @@ public void onClick(View view) { compositeDisposable.add(YouTubeTasks.refreshSubscribedChannel(channel.getChannelId(), null).subscribe()); } compositeDisposable.add(DatabaseTasks.subscribeToChannel(!isUserSubscribed, - this, getContext(), channel, true).subscribe()); + this, getContext(), channel.getChannelId(), true).subscribe()); } } diff --git a/app/src/main/java/free/rm/skytube/gui/fragments/ChannelBrowserFragment.java b/app/src/main/java/free/rm/skytube/gui/fragments/ChannelBrowserFragment.java index f764d22dcb..366f6ce17a 100644 --- a/app/src/main/java/free/rm/skytube/gui/fragments/ChannelBrowserFragment.java +++ b/app/src/main/java/free/rm/skytube/gui/fragments/ChannelBrowserFragment.java @@ -143,7 +143,7 @@ public void onPageScrollStateChanged(int state) { if (userSubscribed != null && channel != null) { startAnimation(view); disposable.add( - DatabaseTasks.subscribeToChannel(!userSubscribed, ChannelBrowserFragment.this, getContext(), channel, true).subscribe(result -> { + DatabaseTasks.subscribeToChannel(!userSubscribed, ChannelBrowserFragment.this, getContext(), channelId, true).subscribe(result -> { ViewCompat.animate(view).setDuration(200); view.setRotation(0); }) @@ -224,7 +224,7 @@ private void getChannelParameters() { // In the event this fragment is passed a channel id and not a channel object, set the // channel the subscribe button is for since there wasn't a channel object to set when // the button was created. - channel = youTubeChannel; + channel = youTubeChannel.channel(); initViews(); })); } else { diff --git a/app/src/main/java/free/rm/skytube/gui/fragments/YouTubePlayerV1Fragment.java b/app/src/main/java/free/rm/skytube/gui/fragments/YouTubePlayerV1Fragment.java index 5a7b6f75cd..1189cdd7ec 100644 --- a/app/src/main/java/free/rm/skytube/gui/fragments/YouTubePlayerV1Fragment.java +++ b/app/src/main/java/free/rm/skytube/gui/fragments/YouTubePlayerV1Fragment.java @@ -428,7 +428,7 @@ private void getVideoInfoTasks() { // get Channel info (e.g. avatar...etc) task compositeDisposable.add(DatabaseTasks.getChannelInfo(requireContext(), youTubeVideo.getChannelId(), false) .subscribe(youTubeChannel1 -> { - youTubeChannel = youTubeChannel1; + youTubeChannel = youTubeChannel1.channel(); videoDescriptionBinding.videoDescSubscribeButton.setChannel(youTubeChannel); if (youTubeChannel != null) { diff --git a/app/src/main/java/free/rm/skytube/gui/fragments/YouTubePlayerV2Fragment.java b/app/src/main/java/free/rm/skytube/gui/fragments/YouTubePlayerV2Fragment.java index 10e2743ad4..85e3cfb60d 100644 --- a/app/src/main/java/free/rm/skytube/gui/fragments/YouTubePlayerV2Fragment.java +++ b/app/src/main/java/free/rm/skytube/gui/fragments/YouTubePlayerV2Fragment.java @@ -784,7 +784,7 @@ private void fetchVideoInformations() { compositeDisposable.add( DatabaseTasks.getChannelInfo(requireContext(), youTubeVideo.getChannelId(), false) .subscribe(youTubeChannel1 -> { - youTubeChannel = youTubeChannel1; + youTubeChannel = youTubeChannel1.channel(); videoDescriptionBinding.videoDescSubscribeButton.setChannel(youTubeChannel); if (youTubeChannel != null) { From 3305501262a196acf917337af9cdd374757259c1 Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Wed, 24 Jul 2024 23:05:04 +0200 Subject: [PATCH 02/10] Further fixes to use PersistentChannel more --- .../YouTube/POJOs/YouTubeChannel.java | 2 +- .../businessobjects/YouTube/YouTubeTasks.java | 2 +- .../newpipe/GetPlaylistsForChannel.java | 3 - .../YouTube/newpipe/NewPipeService.java | 21 ++- .../newpipe/VideoPagerWithChannel.java | 16 +++ .../businessobjects/db/DatabaseTasks.java | 3 +- .../businessobjects/db/LocalChannelTable.java | 4 +- .../businessobjects/db/SubscriptionsDb.java | 132 +++++++++--------- 8 files changed, 97 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java index 0352eacb27..3edc701841 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java @@ -127,7 +127,7 @@ public void setUserSubscribed(boolean userSubscribed) { } public Disposable updateLastVisitTime() { - return SubscriptionsDb.getSubscriptionsDb().updateLastVisitTimeAsync(id).subscribe(timestamp -> { + return SubscriptionsDb.getSubscriptionsDb().updateLastVisitTimeAsync(getChannelId()).subscribe(timestamp -> { lastVisitTime = timestamp; if (lastVisitTime < 0) { Logger.e(YouTubeChannel.this, "Unable to update channel's last visit time. ChannelID=" + id); diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java index 389dd37af6..22dc5e3a96 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java @@ -170,7 +170,7 @@ private static Single getBulkSubscriptionVideos(@NonNull List getChannelVideos(String channelId) throws NewPipeException { SkyTubeApp.nonUiThread(); VideoPagerWithChannel pager = getChannelPager(channelId); - List result = pager.getNextPageAsVideos(); + List result = pager.getNextPageAsVideosAndUpdateChannel(null).channel().getYouTubeVideos(); Logger.i(this, "getChannelVideos for %s(%s) -> %s videos", pager.getChannel().getTitle(), channelId, result.size()); return result; } @@ -271,6 +271,7 @@ public VideoPager getTrending() throws NewPipeException { public VideoPagerWithChannel getChannelPager(String channelId) throws NewPipeException { try { + Logger.e(this, "fetching channel info: "+ channelId); ChannelWithExtractor channelExtractor = getChannelWithExtractor(channelId); return new VideoPagerWithChannel(streamingService, channelExtractor.findVideosTab(), channelExtractor.channel); } catch (ParsingException | RuntimeException e) { @@ -311,26 +312,24 @@ public CommentPager getCommentPager(String videoId) throws NewPipeException { } /** - * Return detailed information for a channel from it's id. - * @param channelId + * Return detailed, fresh information for a channel from it's id. + * @param persistentChannel * @return the {@link YouTubeChannel}, with a list of recent videos. * @throws ExtractionException * @throws IOException */ - public YouTubeChannel getChannelDetails(ChannelId channelId) throws NewPipeException { + public PersistentChannel getChannelDetails(PersistentChannel persistentChannel) throws NewPipeException { + ChannelId channelId = Objects.requireNonNull(persistentChannel.channel().getChannelId(), "channelId"); Logger.i(this, "Fetching channel details for " + channelId); - VideoPagerWithChannel pager = getChannelPager(Objects.requireNonNull(channelId, "channelId").getRawId()); + VideoPagerWithChannel pager = getChannelPager(channelId.getRawId()); // get the channel, and add all the videos from the first page YouTubeChannel channel = pager.getChannel(); try { - List videos = pager.getNextPageAsVideos(); - channel.getYouTubeVideos().addAll(videos); - long lastPublish = videos.stream().mapToLong(YouTubeVideo::getPublishTimestamp).max().orElse(0); - channel.setLastVideoTime(lastPublish); + return pager.getNextPageAsVideosAndUpdateChannel(persistentChannel); } catch (NewPipeException e) { Logger.e(this, "Unable to retrieve videos for "+channelId+", error: "+e.getMessage(), e); + throw e; } - return channel; } private YouTubeChannel createInternalChannelFromFeed(FeedExtractor extractor) throws ParsingException { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java index 3cb96735e4..06b2844f89 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java @@ -21,9 +21,13 @@ import org.schabi.newpipe.extractor.StreamingService; import org.schabi.newpipe.extractor.stream.StreamInfoItem; +import java.util.List; + import free.rm.skytube.businessobjects.Logger; +import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeVideo; +import free.rm.skytube.businessobjects.db.SubscriptionsDb; public class VideoPagerWithChannel extends VideoPager { private final YouTubeChannel channel; @@ -48,4 +52,16 @@ protected YouTubeVideo convert(StreamInfoItem item, String id) { item.getViewCount(), date.instant, date.exact, NewPipeService.getThumbnailUrl(id)); } + /** + * Fetch the first page of videos from the channel, and update the subscription database with the + * fresh data. + */ + public PersistentChannel getNextPageAsVideosAndUpdateChannel(PersistentChannel persistentChannel) throws NewPipeException { + List videos = getNextPageAsVideos(); + channel.getYouTubeVideos().addAll(videos); + long lastPublish = videos.stream().mapToLong(YouTubeVideo::getPublishTimestamp).max().orElse(0); + channel.setLastVideoTime(lastPublish); + return SubscriptionsDb.getSubscriptionsDb().cacheChannel(persistentChannel, channel); + } + } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java index ccf2aef632..44a2704f32 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java @@ -79,8 +79,7 @@ public static PersistentChannel getChannelOrRefresh(Context context, ChannelId c needsRefresh = persistentChannel.channel().getLastCheckTime() < System.currentTimeMillis() - (24 * 60 * 60 * 1000L); } if (needsRefresh && SkyTubeApp.isConnected(context)) { - YouTubeChannel freshChannel = NewPipeService.get().getChannelDetails(channelId); - return db.cacheChannel(persistentChannel, freshChannel); + return NewPipeService.get().getChannelDetails(persistentChannel); } return persistentChannel; } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java index fe21cc93fe..9dc79b0fb1 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java @@ -70,7 +70,7 @@ public static final void addIdColumn(SQLiteDatabase db) { } public static void updateLatestVideoTimestamp(SQLiteDatabase db, PersistentChannel persistentChannel, long latestPublishTimestamp) { - db.execSQL("update " + TABLE_NAME + " set " + COL_LAST_VIDEO_TS + " = max(?, "+COL_LAST_VIDEO_TS+") where " + COL_ID.name + " = ?", new Object[] { - latestPublishTimestamp, persistentChannel.subscriptionPk() }); + db.execSQL("update " + TABLE_NAME + " set " + COL_LAST_VIDEO_TS + " = max(?, coalesce(" + COL_LAST_VIDEO_TS + ",0)) where " + COL_ID.name + " = ?", new Object[]{ + latestPublishTimestamp, persistentChannel.subscriptionPk()}); } } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index 574eeac2fb..1bd9bd1e5a 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java @@ -75,6 +75,8 @@ public class SubscriptionsDb extends SQLiteOpenHelperEx { private static final String GET_ALL_SUBSCRIBED_CHANNEL_ID = "SELECT "+SubscriptionsTable.COL_CHANNEL_ID + " FROM "+SubscriptionsTable.TABLE_NAME; private static final String IS_SUBSCRIBED_QUERY = String.format("SELECT EXISTS(SELECT %s FROM %s WHERE %s =?) AS VAL ", SubscriptionsTable.COL_ID, SubscriptionsTable.TABLE_NAME, SubscriptionsTable.COL_CHANNEL_ID); + private static final String GET_PK_FROM_CHANNEL_ID = "SELECT " + SubscriptionsTable.COL_ID + " FROM " + SubscriptionsTable.TABLE_NAME + " WHERE " + SubscriptionsTable.COL_CHANNEL_ID + " = ?"; + private static volatile SubscriptionsDb subscriptionsDb = null; private static final int DATABASE_VERSION = 15; @@ -305,13 +307,6 @@ private DatabaseResult saveSubscription(ChannelId channelId) { ContentValues values = new ContentValues(); values.put(SubscriptionsTable.COL_CHANNEL_ID, channelId.getRawId()); -// values.put(SubscriptionsTable.COL_LAST_VISIT_TIME, channel.getLastVisitTime()); -// values.put(SubscriptionsTable.COL_TITLE, channel.getTitle()); -// values.put(SubscriptionsTable.COL_DESCRIPTION, channel.getDescription()); -// values.put(SubscriptionsTable.COL_BANNER_URL, channel.getBannerUrl()); -// values.put(SubscriptionsTable.COL_THUMBNAIL_NORMAL_URL, channel.getThumbnailUrl()); -// values.put(SubscriptionsTable.COL_CATEGORY_ID.name, channel.getCategoryId()); -// values.put(SubscriptionsTable.COL_SUBSCRIBER_COUNT, channel.getSubscriberCount()); SQLiteDatabase db = getWritableDatabase(); try { @@ -342,15 +337,15 @@ private DatabaseResult saveSubscription(ChannelId channelId) { public DatabaseResult unsubscribe(PersistentChannel channel) { SkyTubeApp.nonUiThread(); - // delete any feed videos pertaining to this channel - getWritableDatabase().delete(SubscriptionsVideosTable.TABLE_NAME_V2, - SubscriptionsVideosTable.COL_SUBS_ID.name + " = ?", - new String[]{String.valueOf(channel.channelPk())}); + // delete any feed videos pertaining to this channel + getWritableDatabase().delete(SubscriptionsVideosTable.TABLE_NAME_V2, + SubscriptionsVideosTable.COL_SUBS_ID.name + " = ?", + toArray(channel.channelPk())); - // remove this channel from the subscriptions DB - int rowsDeleted = getWritableDatabase().delete(SubscriptionsTable.TABLE_NAME, - SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channel.channel().getId()}); + // remove this channel from the subscriptions DB + int rowsDeleted = getWritableDatabase().delete(SubscriptionsTable.TABLE_NAME, + SubscriptionsTable.COL_CHANNEL_ID + " = ?", + toArray(channel.channel().getId())); // Need to make sure when we come back to MainActivity, that we refresh the Feed tab so it hides videos from the newly unsubscribed SkyTubeApp.getSettings().setRefreshSubsFeedFromCache(true); @@ -369,7 +364,7 @@ public void unsubscribeFromAllChannels() { */ public Set getSubscribedChannelVideosByChannel(ChannelId channelId) { SkyTubeApp.nonUiThread(); - try(Cursor cursor = getReadableDatabase().rawQuery(GET_VIDEO_IDS_BY_CHANNEL, new String[] { channelId.getRawId()})) { + try (Cursor cursor = getReadableDatabase().rawQuery(GET_VIDEO_IDS_BY_CHANNEL, toArrayParam(channelId))) { Set result = new HashSet<>(); while(cursor.moveToNext()) { result.add(cursor.getString(0)); @@ -384,7 +379,7 @@ public Set getSubscribedChannelVideosByChannel(ChannelId channelId) { */ public Map getSubscribedChannelVideosByChannelToTimestamp(ChannelId channelId) { SkyTubeApp.nonUiThread(); - try(Cursor cursor = getReadableDatabase().rawQuery(GET_VIDEO_IDS_BY_CHANNEL_TO_PUBLISH_TS, new String[] { channelId.getRawId()})) { + try(Cursor cursor = getReadableDatabase().rawQuery(GET_VIDEO_IDS_BY_CHANNEL_TO_PUBLISH_TS, toArrayParam(channelId))) { Map result = new HashMap<>(); while(cursor.moveToNext()) { result.put(cursor.getString(0), cursor.getLong(1)); @@ -517,10 +512,9 @@ private Integer getInteger(Cursor cursor, int colCategoryId) { public boolean isUserSubscribedToChannel(ChannelId channelId) { SkyTubeApp.nonUiThread(); - return executeQueryForInteger(IS_SUBSCRIBED_QUERY, new String[]{channelId.getRawId()}, 0) > 0; + return executeQueryForInteger(IS_SUBSCRIBED_QUERY, toArrayParam(channelId), 0) > 0; } - public Single getUserSubscribedToChannel(ChannelId channelId) { return Single.fromCallable(() -> isUserSubscribedToChannel(channelId)).subscribeOn(Schedulers.io()); } @@ -532,7 +526,7 @@ public Single getUserSubscribedToChannel(ChannelId channelId) { * * @return last visit time, if the update was successful; -1 otherwise. */ - public Single updateLastVisitTimeAsync(String channelId) { + public Single updateLastVisitTimeAsync(ChannelId channelId) { return Single.fromCallable(() -> { SQLiteDatabase db = getWritableDatabase(); long currentTime = System.currentTimeMillis(); @@ -544,7 +538,7 @@ public Single updateLastVisitTimeAsync(String channelId) { SubscriptionsTable.TABLE_NAME, values, SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channelId}); + toArrayParam(channelId)); return (count > 0 ? currentTime : -1); }).subscribeOn(Schedulers.io()); @@ -608,26 +602,6 @@ public void saveChannelVideos(Collection videos, PersistentChannel LocalChannelTable.updateLatestVideoTimestamp(db, persistentChannel, latestPublishTimestamp); } - /** - * Insert videos into the subscription video table. - * @param videos - */ - public void insertVideosForChannel(List videos, PersistentChannel channel) { - SkyTubeApp.nonUiThread(); - - SQLiteDatabase db = getWritableDatabase(); - for (YouTubeVideo video : videos) { - try { - if (video.getPublishTimestamp() != null) { - ContentValues values = createContentValues(video, channel); - db.insert(SubscriptionsVideosTable.TABLE_NAME_V2, null, values); - } - } catch (Exception e) { - Logger.e(this, e, "Error inserting "+ videos + " - "+e.getMessage()); - } - } - } - private ContentValues createContentValues(YouTubeVideo video, PersistentChannel persistentChannel) { ContentValues values = convertToContentValues(video, persistentChannel); values.put(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, video.getId()); @@ -736,10 +710,10 @@ public Maybe getChannel(ChannelId channelId) { */ public PersistentChannel getCachedChannel(ChannelId channelId) { SkyTubeApp.nonUiThread(); - try (Cursor cursor = getReadableDatabase().rawQuery( + try (Cursor cursor = getReadableDatabase().rawQuery( "select s._id subs_id, c.* from Channel c left outer Join Subs s on c.Channel_Id = s.Channel_Id where c.Channel_Id = ?", - new String[] { channelId.getRawId() })) { - if (cursor.moveToNext()) { + toArrayParam(channelId))) { + if (cursor.moveToNext()) { Long subscriptionPk = getOptionalLong(cursor, "subs_id"); Long channelPk = getLong(cursor, LocalChannelTable.COL_ID.name); @@ -769,7 +743,48 @@ public PersistentChannel cacheChannel(@Nullable PersistentChannel persistentChan return cacheChannel(db, persistentChannel, channel); } + private String[] toArray(Object obj) { + return new String[] { String.valueOf(obj)}; + } + + private String[] toArrayParam(ChannelId channelId) { + return new String[] { channelId.getRawId() }; + } + + private Long getChannelPk(SQLiteDatabase db, ChannelId channelId){ + try (Cursor cursor = db.rawQuery(GET_PK_FROM_CHANNEL_ID, toArrayParam(channelId))) { + if (cursor.moveToNext()) { + return cursor.getLong(0); + } + } + return null; + } + private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentChannel persistentChannel, YouTubeChannel channel) { + ContentValues values = toContentValues(channel); + + Long channelPk = persistentChannel != null ? persistentChannel.channelPk() : getChannelPk(db, channel.getChannelId()); + Long subPk = persistentChannel != null ? persistentChannel.subscriptionPk() : null; + + // If there is a persistentChannel info, we already have the channel in the db + if (channelPk != null) { + // Try to update it ... + int count = db.update( + LocalChannelTable.TABLE_NAME, + values, + LocalChannelTable.COL_ID.name + " = ?", + toArray(channelPk)); + if (count != 1) { + throw new IllegalStateException("Unable to update channel " + channel + ", with pk= " + channelPk); + } + return new PersistentChannel(channel, channelPk, subPk); + } + values.put(LocalChannelTable.COL_CHANNEL_ID, channel.getChannelId().getRawId()); + long newPk = db.insert(LocalChannelTable.TABLE_NAME, null, values); + return new PersistentChannel(channel, newPk, subPk); + } + + private static ContentValues toContentValues(YouTubeChannel channel) { ContentValues values = new ContentValues(); values.put(LocalChannelTable.COL_TITLE, channel.getTitle()); if (!Utils.isEmpty(channel.getDescription())) { @@ -784,31 +799,16 @@ private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentCh if (channel.getSubscriberCount() > 0) { values.put(LocalChannelTable.COL_SUBSCRIBER_COUNT, channel.getSubscriberCount()); } - if(channel.getLastVideoTime() > 0) { - values.put(LocalChannelTable.COL_LAST_VIDEO_TS, channel.getLastVideoTime()); - } - if (channel.getLastCheckTime() > 0) { - values.put(LocalChannelTable.COL_LAST_CHECK_TS, channel.getLastCheckTime()); - } - - // If there is a persistentChannel info, we already have the channel in the db - if (persistentChannel != null) { - // Try to update it ... - int count = db.update( - LocalChannelTable.TABLE_NAME, - values, - LocalChannelTable.COL_ID.name + " = ?", - new String[]{ String.valueOf(persistentChannel.channelPk())}); - if (count > 0) { - return persistentChannel.with(channel); - } + if(channel.getLastVideoTime() > 0) { + values.put(LocalChannelTable.COL_LAST_VIDEO_TS, channel.getLastVideoTime()); } - values.put(LocalChannelTable.COL_CHANNEL_ID, channel.getId()); - long newPk = db.insert(LocalChannelTable.TABLE_NAME, null, values); - return new PersistentChannel(channel, newPk, null); + if (channel.getLastCheckTime() > 0) { + values.put(LocalChannelTable.COL_LAST_CHECK_TS, channel.getLastCheckTime()); + } + return values; } - public List getSubscribedChannelsByText(String searchText, boolean sortChannelsAlphabetically) { + public List getSubscribedChannelsByText(String searchText, boolean sortChannelsAlphabetically) { List result = new ArrayList<>(); try (Cursor cursor = createSubscriptionCursor(searchText, sortChannelsAlphabetically)) { final int channelId = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CHANNEL_ID); From 426a6832508709f8cf2f1b3af33e5c7a430eb0ea Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Thu, 9 Nov 2023 00:52:35 +0100 Subject: [PATCH 03/10] Remove the annoying spinner during feed refresh, do everything in the background and display the already loaded videos instantly --- .../free/rm/skytube/app/FeedUpdateTask.java | 2 +- .../businessobjects/YouTube/VideoBlocker.java | 14 ++--- .../businessobjects/YouTube/YouTubeTasks.java | 23 ++++--- .../adapters/VideoGridAdapter.java | 2 +- .../fragments/SubscriptionsFeedFragment.java | 60 +++---------------- 5 files changed, 32 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/free/rm/skytube/app/FeedUpdateTask.java b/app/src/main/java/free/rm/skytube/app/FeedUpdateTask.java index 095d791b60..908c788ed8 100644 --- a/app/src/main/java/free/rm/skytube/app/FeedUpdateTask.java +++ b/app/src/main/java/free/rm/skytube/app/FeedUpdateTask.java @@ -65,10 +65,10 @@ public synchronized boolean start(Context context) { if (refreshInProgress) { return false; } - createNotificationChannel(context); if (!SkyTubeApp.isConnected(context)) { return false; } + createNotificationChannel(context); SkyTubeApp.getSettings().setRefreshSubsFeedFull(false); refreshInProgress = true; diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/VideoBlocker.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/VideoBlocker.java index 7df7d5ae82..4f18dd28f2 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/VideoBlocker.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/VideoBlocker.java @@ -60,7 +60,7 @@ public class VideoBlocker { /** Default preferred language(s) -- by default, no language shall be filtered out. */ private static final Set defaultPrefLanguages = new HashSet<>(SkyTubeApp.getStringArrayAsList(R.array.languages_iso639_codes)); - private Settings settings; + private final Settings settings; public VideoBlocker() { settings = SkyTubeApp.getSettings(); @@ -119,7 +119,7 @@ public List filter(List videosList) { * @return True if the user wants to use the video blocker, false otherwise. */ private boolean isVideoBlockerEnabled() { - return SkyTubeApp.getSettings().isEnableVideoBlocker(); + return settings.isEnableVideoBlocker(); } /** @@ -448,9 +448,9 @@ public String toString() { */ public static class BlockedVideo implements Serializable { - private YouTubeVideo video; - private FilterType filteringType; - private String reason; + private final YouTubeVideo video; + private final FilterType filteringType; + private final String reason; BlockedVideo(YouTubeVideo video, FilterType filteringType, String reason) { @@ -496,8 +496,8 @@ public interface VideoBlockerListener { private static class LanguageDetectionSingleton { private static LanguageDetectionSingleton languageDetectionSingleton = null; - private TextObjectFactory textObjectFactory; - private LanguageDetector languageDetector; + private final TextObjectFactory textObjectFactory; + private final LanguageDetector languageDetector; private LanguageDetectionSingleton() throws IOException { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java index 22dc5e3a96..a9b2b3d85e 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java @@ -408,19 +408,20 @@ public static Maybe> getYouTubeVideos(@NonNull GetYouTubeVideos g if (swipeRefreshLayout != null) { swipeRefreshLayout.setRefreshing(true); } + final boolean subscriptionFeedVideos = videoGridAdapter.getCurrentVideoCategory() == VideoCategory.SUBSCRIPTIONS_FEED_VIDEOS; return Maybe.fromCallable(() -> { // get videos from YouTube or the database. - List videosList; + final List videosList; - if (clearList && videoGridAdapter.getCurrentVideoCategory() == VideoCategory.SUBSCRIPTIONS_FEED_VIDEOS) { + if (clearList && subscriptionFeedVideos) { final int currentSize = videoGridAdapter.getItemCount(); List result = new ArrayList<>(currentSize); boolean hasNew; do { - videosList = getYouTubeVideos.getNextVideos(); - hasNew = !videosList.isEmpty(); - result.addAll(videosList); + final List nextVideos = getYouTubeVideos.getNextVideos(); + hasNew = !nextVideos.isEmpty(); + result.addAll(nextVideos); } while(result.size() < currentSize && hasNew); videosList = result; } else { @@ -429,12 +430,16 @@ public static Maybe> getYouTubeVideos(@NonNull GetYouTubeVideos g if (videosList != null) { // filter videos + final List filteredVideos; if (videoGridAdapter.getCurrentVideoCategory().isVideoFilteringEnabled()) { - videosList = new VideoBlocker().filter(videosList); + filteredVideos = new VideoBlocker().filter(videosList); + } else { + filteredVideos = videosList; } + // This is not used for subscriptionFeedVideos if (channel != null && channel.isUserSubscribed()) { - for (CardData video : videosList) { + for (CardData video : filteredVideos) { if (video instanceof YouTubeVideo) { channel.addYouTubeVideo((YouTubeVideo) video); } @@ -443,9 +448,11 @@ public static Maybe> getYouTubeVideos(@NonNull GetYouTubeVideos g PersistentChannel persistentChannel = db.getCachedChannel(channel.getChannelId()); db.saveChannelVideos(channel.getYouTubeVideos(), persistentChannel, false); } + return filteredVideos; + } else { + return Collections.emptyList(); } - return videosList; }) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/VideoGridAdapter.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/VideoGridAdapter.java index f15e0b4be7..24487cdc88 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/VideoGridAdapter.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/VideoGridAdapter.java @@ -210,7 +210,7 @@ public void refresh() { * @param clearVideosList If set to true, it will clear out any previously loaded videos (found * in this adapter). */ - public synchronized void refresh(boolean clearVideosList) { + public final synchronized void refresh(boolean clearVideosList) { if (getYouTubeVideos != null && !refreshHappens) { refreshHappens = true; if (clearVideosList) { diff --git a/app/src/main/java/free/rm/skytube/gui/fragments/SubscriptionsFeedFragment.java b/app/src/main/java/free/rm/skytube/gui/fragments/SubscriptionsFeedFragment.java index cfa0273dff..c1786e94b5 100644 --- a/app/src/main/java/free/rm/skytube/gui/fragments/SubscriptionsFeedFragment.java +++ b/app/src/main/java/free/rm/skytube/gui/fragments/SubscriptionsFeedFragment.java @@ -30,15 +30,13 @@ import androidx.annotation.Nullable; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import com.afollestad.materialdialogs.MaterialDialog; - - import free.rm.skytube.R; import free.rm.skytube.app.EventBus; import free.rm.skytube.app.FeedUpdateTask; import free.rm.skytube.app.Settings; import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.businessobjects.FeedUpdaterService; +import free.rm.skytube.businessobjects.Logger; import free.rm.skytube.businessobjects.VideoCategory; import free.rm.skytube.businessobjects.YouTube.Tasks.GetSubscriptionVideosTaskListener; import free.rm.skytube.databinding.FragmentSubsFeedBinding; @@ -65,7 +63,6 @@ public void onReceive(Context context, Intent intent) { private FragmentSubsFeedBinding binding; private final CompositeDisposable compositeDisposable = new CompositeDisposable(); - private MaterialDialog fetchingChannelInfoDialog; @Override public void onCreate(@Nullable Bundle savedInstanceState) { @@ -101,22 +98,15 @@ public void onResume() { // setup the UI and refresh the feed (if applicable) Settings settings = SkyTubeApp.getSettings(); - startRefreshTask(isFragmentSelected(), settings.isFullRefreshTimely() || settings.isRefreshSubsFeedFull()); - - // this will detect whether we have previous instructed the app (via refreshSubsFeedFromCache()) - // to refresh the subs feed - if (settings.isRefreshSubsFeedFromCache()) { - // unset the flag - settings.setRefreshSubsFeedFromCache(false); - - // refresh the subs feed by reading from the cache (i.e. local DB) - refreshFeedFromCache(); + if (settings.isFullRefreshTimely() || settings.isRefreshSubsFeedFull()) { + startRefreshTask(); } + + refreshFeedFromCache(); } @Override public synchronized void onPause() { - hideFetchingVideosDialog(); super.onPause(); requireActivity().unregisterReceiver(feedUpdaterReceiver); } @@ -138,28 +128,11 @@ public void onDestroyView() { @Override public void onRefresh() { - startRefreshTask(false, true); + startRefreshTask(); } - - private synchronized void startRefreshTask(boolean isShowFetchingVideosDialog, boolean forcedFullRefresh) { - FeedUpdateTask fut = FeedUpdateTask.getInstance(); - if (fut.isRefreshInProgress()) { - if (isShowFetchingVideosDialog) { - showFetchingVideosDialog(); - } - return; - } - if (forcedFullRefresh && SkyTubeApp.isConnected(requireContext())) { - if (isShowFetchingVideosDialog) { - showFetchingVideosDialog(); - } - - fut.start(requireContext()); - - } else { - videoGridAdapter.refresh(true); - } + private synchronized void startRefreshTask() { + FeedUpdateTask.getInstance().start(requireContext()); } @Override @@ -180,7 +153,6 @@ public void onSubscriptionRefreshFinished() { if (gridviewBinding.swipeRefreshLayout != null) { gridviewBinding.swipeRefreshLayout.setRefreshing(false); } - hideFetchingVideosDialog(); } @Override @@ -234,22 +206,6 @@ private void setupUiAccordingToNumOfSubbedChannels(boolean hasChannels) { } } - private synchronized void hideFetchingVideosDialog() { - if (fetchingChannelInfoDialog != null) { - fetchingChannelInfoDialog.dismiss(); - fetchingChannelInfoDialog = null; - } - } - - private synchronized void showFetchingVideosDialog() { - hideFetchingVideosDialog(); - fetchingChannelInfoDialog = new MaterialDialog.Builder(getActivity()) - .content(R.string.fetching_subbed_channels_info) - .progress(true, 0) - .build(); - fetchingChannelInfoDialog.show(); - } - public void refreshFeedFromCache() { if (videoGridAdapter != null) { videoGridAdapter.refresh(true); From ca0232356a06ab7098622d662a9d4182265d6ea7 Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Fri, 26 Jul 2024 11:39:02 +0200 Subject: [PATCH 04/10] Add publish time index to the subscription-videos table --- .../businessobjects/db/LocalChannelTable.java | 10 ++++---- .../db/SQLiteOpenHelperEx.java | 10 ++++++++ .../businessobjects/db/SubscriptionsDb.java | 23 +++++++++++-------- .../db/SubscriptionsTable.java | 8 +++---- .../db/SubscriptionsVideosTable.java | 16 ++++++++++++- 5 files changed, 45 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java index 9dc79b0fb1..ce8624b0c8 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java @@ -37,7 +37,7 @@ public class LocalChannelTable { static final String GET_ID_AND_CHANNEL_ID = String.format("SELECT %s, %s FROM %s", LocalChannelTable.COL_ID.name, LocalChannelTable.COL_CHANNEL_ID, LocalChannelTable.TABLE_NAME); - public static final String[] ALL_COLUMNS = new String[]{ + private static final String[] ALL_COLUMNS = new String[]{ LocalChannelTable.COL_CHANNEL_ID, LocalChannelTable.COL_TITLE, LocalChannelTable.COL_DESCRIPTION, @@ -62,11 +62,9 @@ public static String getCreateStatement(boolean withPk) { } public static final void addIdColumn(SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE_NAME + " RENAME TO old_" + TABLE_NAME); - db.execSQL(getCreateStatement(true)); - String allRowNames = Joiner.on(',').join(ALL_COLUMNS); - db.execSQL("insert into " + TABLE_NAME + " (_id," + allRowNames + ") select rowid," + allRowNames + " from old_" + TABLE_NAME); - db.execSQL("DROP TABLE old_" + TABLE_NAME); + final String allRowNames = Joiner.on(',').join(ALL_COLUMNS); + SQLiteOpenHelperEx.updateTableSchema(db, TABLE_NAME, getCreateStatement(true), + "insert into " + TABLE_NAME + " (_id," + allRowNames + ") select rowid," + allRowNames); } public static void updateLatestVideoTimestamp(SQLiteDatabase db, PersistentChannel persistentChannel, long latestPublishTimestamp) { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java index 25b7b295a6..f2e10cd1b2 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java @@ -23,7 +23,10 @@ import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; +import com.google.common.base.Joiner; + import java.io.File; +import java.util.function.Supplier; import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.businessobjects.Logger; @@ -160,6 +163,13 @@ public static void createTable(SQLiteDatabase db, String tableName, Column... co } } + public static void updateTableSchema(SQLiteDatabase db, String tableName, String newTableCreateStatement, String migration) { + db.execSQL("ALTER TABLE " + tableName + " RENAME TO old_" + tableName); + db.execSQL(newTableCreateStatement); + db.execSQL(migration + " from old_" + tableName); + db.execSQL("DROP TABLE old_" + tableName); + } + private static String listColumns(boolean justNames, final Column[] columns) { boolean first = true; final StringBuilder sql = new StringBuilder(); diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index 1bd9bd1e5a..faebec8e1e 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java @@ -79,7 +79,7 @@ public class SubscriptionsDb extends SQLiteOpenHelperEx { private static volatile SubscriptionsDb subscriptionsDb = null; - private static final int DATABASE_VERSION = 15; + private static final int DATABASE_VERSION = 16; private static final String DATABASE_NAME = "subs.db"; @@ -99,6 +99,7 @@ public static synchronized SubscriptionsDb getSubscriptionsDb() { public void onCreate(SQLiteDatabase db) { db.execSQL(SubscriptionsTable.getCreateStatement()); SubscriptionsVideosTable.addNewFlatTable(db); + SubscriptionsVideosTable.addPublishTimeIndex(db); db.execSQL(LocalChannelTable.getCreateStatement(true)); db.execSQL(CategoriesTable.getCreateStatement()); new CategoryManagement(db).setupDefaultCategories(); @@ -165,6 +166,9 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (upgrade.executeStep(15)) { SubscriptionsTable.cleanupTable(db); } + if (upgrade.executeStep(16)) { + SubscriptionsVideosTable.addPublishTimeIndex(db); + } } private void normalizeSubscriptionVideosTable(final SQLiteDatabase db) { @@ -623,20 +627,19 @@ public List getSubscriptionVideoPage(int limit, String videoId, lo final String sortingColumn = SubscriptionsVideosTable.COL_PUBLISH_TIME.name; final String[] selectionArguments; if (videoId != null) { - selection = "(" + sortingColumn + " < ?) OR (" + sortingColumn + " = ? AND " + SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID + " > ?)"; + selection = "WHERE (" + sortingColumn + " < ?) OR (" + sortingColumn + " = ? AND " + SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID + " > ?)"; String formatted = String.valueOf(beforeTimestamp); selectionArguments = new String[]{ formatted, formatted, videoId }; } else { - selection = null; + selection = ""; selectionArguments = null; } - Cursor cursor = getReadableDatabase().query( - SubscriptionsVideosTable.TABLE_NAME_V2, - SubscriptionsVideosTable.ALL_COLUMNS_FOR_EXTRACT, - selection, selectionArguments, null, null, - sortingColumn + " DESC, " + SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID + " ASC", - String.valueOf(limit)); - return extractVideos(cursor, true); + final String sorting = " ORDER BY " + sortingColumn + " DESC, " + SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID + " ASC limit "+ limit; + String query = SubscriptionsVideosTable.BASE_QUERY + selection + sorting; + try (Stopwatch s = new Stopwatch("getVideos " + query + ",limit=" + limit + ", beforeTimestamp=" + beforeTimestamp+" videoid="+videoId)) { + Cursor cursor = getReadableDatabase().rawQuery(query, selectionArguments); + return extractVideos(cursor, true); + } } /** diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java index 21a424b09a..55cfc24cbc 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java @@ -72,11 +72,9 @@ public static String[] getLastCheckTimeColumn() { } public static void cleanupTable(SQLiteDatabase db) { - db.execSQL("ALTER TABLE " + TABLE_NAME + " RENAME TO old_" + TABLE_NAME); - db.execSQL(getCreateStatement()); - db.execSQL("insert into " + TABLE_NAME + " (_id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_VIDEO_FETCH + - ") select _id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_CHECK_TIME + " from old_" + TABLE_NAME); - db.execSQL("DROP TABLE old_" + TABLE_NAME); + SQLiteOpenHelperEx.updateTableSchema(db, TABLE_NAME, getCreateStatement(), + "insert into " + TABLE_NAME + " (_id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_VIDEO_FETCH + + ") select _id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_CHECK_TIME ); } public static void updateLastVideoFetchTimestamps(SQLiteDatabase db, PersistentChannel persistentChannel) { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java index e4ae7db74c..15549e301c 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java @@ -56,10 +56,10 @@ public class SubscriptionsVideosTable { private static final String IDX_PUBLISH_TS = "IDX_SubsVideo_Publish"; private static final String IDX_PUBLISH_TS_V2 = "IDX_subscription_videos_Publish"; + private static final String IDX_PUBLISH_TIMESTAMP = "IDX_subscription_videos_PublishTime"; static final String[] ALL_COLUMNS_FOR_EXTRACT = new String[] { COL_CHANNEL_ID_V2.name, - COL_CHANNEL_TITLE.name, COL_YOUTUBE_VIDEO_ID_V2.name, COL_CATEGORY_ID.name, COL_TITLE.name, @@ -73,6 +73,17 @@ public class SubscriptionsVideosTable { COL_PUBLISH_TIME_EXACT.name, }; + static final String BASE_QUERY; + static { + StringBuilder s = new StringBuilder("select c.Title channel_title"); + for (String col : ALL_COLUMNS_FOR_EXTRACT) { + s.append(",s.").append(col); + } + s.append(" from subscription_videos s left join Channel c on s.channel_pk = c._id "); + BASE_QUERY = s.toString(); + } + + private static final String ADD_COLUMN = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN "; public static String getCreateStatement() { @@ -138,4 +149,7 @@ static void addChannelPkColumn(SQLiteDatabase db) { SQLiteOpenHelperEx.addColumn(db, TABLE_NAME_V2, COL_CHANNEL_PK); } + static void addPublishTimeIndex(SQLiteDatabase db) { + SQLiteOpenHelperEx.createIndex(db, IDX_PUBLISH_TIMESTAMP, TABLE_NAME_V2, COL_PUBLISH_TIME); + } } From 9835103bbe74005970469e20e174e38c51fbd5fa Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Fri, 9 Aug 2024 08:33:21 +0200 Subject: [PATCH 05/10] Remove channel title from subscription_videos table --- .../YouTube/newpipe/NewPipeService.java | 3 +- .../newpipe/VideoPagerWithChannel.java | 4 ++- .../rm/skytube/businessobjects/db/Column.java | 5 +++- .../businessobjects/db/DatabaseTasks.java | 2 +- .../db/SQLiteOpenHelperEx.java | 12 +++++--- .../businessobjects/db/SubscriptionsDb.java | 22 +++++++++----- .../db/SubscriptionsVideosTable.java | 29 +++++++++++++++---- 7 files changed, 55 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java index 37c087fc02..8c73bd0f39 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java @@ -318,8 +318,7 @@ public CommentPager getCommentPager(String videoId) throws NewPipeException { * @throws ExtractionException * @throws IOException */ - public PersistentChannel getChannelDetails(PersistentChannel persistentChannel) throws NewPipeException { - ChannelId channelId = Objects.requireNonNull(persistentChannel.channel().getChannelId(), "channelId"); + public PersistentChannel getChannelDetails(ChannelId channelId, PersistentChannel persistentChannel) throws NewPipeException { Logger.i(this, "Fetching channel details for " + channelId); VideoPagerWithChannel pager = getChannelPager(channelId.getRawId()); // get the channel, and add all the videos from the first page diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java index 06b2844f89..bbad0e387c 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java @@ -16,6 +16,8 @@ */ package free.rm.skytube.businessobjects.YouTube.newpipe; +import androidx.annotation.Nullable; + import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.StreamingService; @@ -56,7 +58,7 @@ protected YouTubeVideo convert(StreamInfoItem item, String id) { * Fetch the first page of videos from the channel, and update the subscription database with the * fresh data. */ - public PersistentChannel getNextPageAsVideosAndUpdateChannel(PersistentChannel persistentChannel) throws NewPipeException { + public PersistentChannel getNextPageAsVideosAndUpdateChannel(@Nullable PersistentChannel persistentChannel) throws NewPipeException { List videos = getNextPageAsVideos(); channel.getYouTubeVideos().addAll(videos); long lastPublish = videos.stream().mapToLong(YouTubeVideo::getPublishTimestamp).max().orElse(0); diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/Column.java b/app/src/main/java/free/rm/skytube/businessobjects/db/Column.java index 63654b426c..b85546a477 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/Column.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/Column.java @@ -2,7 +2,7 @@ import android.database.Cursor; -class Column { +final class Column { final String name; final String type; @@ -23,6 +23,9 @@ public String format() { return name + ' ' + type + (modifier != null ? " "+ modifier : ""); } + public String name() { + return name; + } public int getColumn(Cursor cursor) { return cursor.getColumnIndex(name); } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java index 44a2704f32..ed0ee533e2 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java @@ -79,7 +79,7 @@ public static PersistentChannel getChannelOrRefresh(Context context, ChannelId c needsRefresh = persistentChannel.channel().getLastCheckTime() < System.currentTimeMillis() - (24 * 60 * 60 * 1000L); } if (needsRefresh && SkyTubeApp.isConnected(context)) { - return NewPipeService.get().getChannelDetails(persistentChannel); + return NewPipeService.get().getChannelDetails(channelId, persistentChannel); } return persistentChannel; } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java index f2e10cd1b2..4077216392 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java @@ -23,10 +23,7 @@ import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; -import com.google.common.base.Joiner; - import java.io.File; -import java.util.function.Supplier; import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.businessobjects.Logger; @@ -154,9 +151,13 @@ public static void createIndex(SQLiteDatabase db, String indexName, String table db.execSQL("CREATE INDEX "+indexName+" ON "+tableName + "("+listColumns(true, columns)+")"); } + public static String getCreateTableCommand(String tableName, Column... columns) { + return "CREATE TABLE " + tableName + " (" + listColumns(false, columns) + ")"; + } + public static void createTable(SQLiteDatabase db, String tableName, Column... columns) { try { - db.execSQL("CREATE TABLE " + tableName + " (" + listColumns(false, columns) + ")"); + db.execSQL(getCreateTableCommand(tableName, columns)); } catch (SQLiteException e) { Logger.e(db, e,"Unable to create table: '%s', because: %s", tableName, e.getMessage()); throw e; @@ -174,6 +175,9 @@ private static String listColumns(boolean justNames, final Column[] columns) { boolean first = true; final StringBuilder sql = new StringBuilder(); for (Column col : columns) { + if (col == null) { + continue; + } if (first) { first = false; } else { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index faebec8e1e..b31aad6923 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java @@ -42,7 +42,7 @@ import java.util.Objects; import java.util.Set; -import javax.annotation.Nullable; +import androidx.annotation.Nullable; import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.app.Utils; @@ -79,7 +79,7 @@ public class SubscriptionsDb extends SQLiteOpenHelperEx { private static volatile SubscriptionsDb subscriptionsDb = null; - private static final int DATABASE_VERSION = 16; + private static final int DATABASE_VERSION = 17; private static final String DATABASE_NAME = "subs.db"; @@ -98,7 +98,7 @@ public static synchronized SubscriptionsDb getSubscriptionsDb() { @Override public void onCreate(SQLiteDatabase db) { db.execSQL(SubscriptionsTable.getCreateStatement()); - SubscriptionsVideosTable.addNewFlatTable(db); + SubscriptionsVideosTable.addNewFlatTable(db, false); SubscriptionsVideosTable.addPublishTimeIndex(db); db.execSQL(LocalChannelTable.getCreateStatement(true)); db.execSQL(CategoriesTable.getCreateStatement()); @@ -145,7 +145,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { SubscriptionsTable.addCategoryColumn(db); } if (upgrade.executeStep(9)) { - SubscriptionsVideosTable.addNewFlatTable(db); + SubscriptionsVideosTable.addNewFlatTable(db, true); migrateFromJsonColumn(db); } if (upgrade.executeStep(10)) { @@ -166,7 +166,9 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (upgrade.executeStep(15)) { SubscriptionsTable.cleanupTable(db); } - if (upgrade.executeStep(16)) { + if (upgrade.executeStep(17)) { + Logger.w(this, "Remove channel title from subscription_videos table"); + SubscriptionsVideosTable.removeChannelTitle(db); SubscriptionsVideosTable.addPublishTimeIndex(db); } } @@ -410,7 +412,6 @@ private ContentValues convertToContentValues(final YouTubeVideo video, @Nullable values.put(SubscriptionsVideosTable.COL_CHANNEL_PK.name, persistentChannel.channelPk()); } - values.put(SubscriptionsVideosTable.COL_CHANNEL_TITLE.name, video.getSafeChannelName()); values.put(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID_V2.name, video.getId()); values.put(SubscriptionsVideosTable.COL_CATEGORY_ID.name, video.getCategoryId()); values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME_EXACT.name, video.getPublishTimestampExact()); @@ -754,7 +755,7 @@ private String[] toArrayParam(ChannelId channelId) { return new String[] { channelId.getRawId() }; } - private Long getChannelPk(SQLiteDatabase db, ChannelId channelId){ + private @Nullable Long getChannelPk(SQLiteDatabase db, ChannelId channelId){ try (Cursor cursor = db.rawQuery(GET_PK_FROM_CHANNEL_ID, toArrayParam(channelId))) { if (cursor.moveToNext()) { return cursor.getLong(0); @@ -766,7 +767,12 @@ private Long getChannelPk(SQLiteDatabase db, ChannelId channelId){ private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentChannel persistentChannel, YouTubeChannel channel) { ContentValues values = toContentValues(channel); - Long channelPk = persistentChannel != null ? persistentChannel.channelPk() : getChannelPk(db, channel.getChannelId()); + final Long channelPk; + if (persistentChannel != null) { + channelPk = persistentChannel.channelPk(); + } else { + channelPk = getChannelPk(db, channel.getChannelId()); + } Long subPk = persistentChannel != null ? persistentChannel.subscriptionPk() : null; // If there is a persistentChannel info, we already have the channel in the db diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java index 15549e301c..265f23d9ad 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java @@ -19,6 +19,11 @@ import android.database.sqlite.SQLiteDatabase; +import com.google.common.base.Joiner; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + /** * A table that caches metadata about videos published by subbed channels. */ @@ -119,15 +124,14 @@ public static String[] getAddTimestampColumns() { public static String getIndexOnVideos() { return "CREATE INDEX " + IDX_PUBLISH_TS + " ON " + TABLE_NAME + "(" + COL_PUBLISH_TS + ")"; } - - public static void addNewFlatTable(SQLiteDatabase db) { - SQLiteOpenHelperEx.createTable(db, TABLE_NAME_V2, + private static Column[] getAllColumns(boolean withChannelTitle) { + return new Column[] { COL_CHANNEL_PK, COL_SUBS_ID, COL_CHANNEL_ID_V2, COL_YOUTUBE_VIDEO_ID_V2, COL_CATEGORY_ID, - COL_CHANNEL_TITLE, + withChannelTitle ? COL_CHANNEL_TITLE : null, COL_TITLE, COL_DESCRIPTION, COL_THUMBNAIL_URL, @@ -137,7 +141,11 @@ public static void addNewFlatTable(SQLiteDatabase db) { COL_DURATION, COL_PUBLISH_TIME, COL_PUBLISH_TIME_EXACT - ); + }; + } + + public static void addNewFlatTable(SQLiteDatabase db, boolean withChannelTitle) { + db.execSQL(SQLiteOpenHelperEx.getCreateTableCommand(TABLE_NAME_V2, getAllColumns(withChannelTitle))); SQLiteOpenHelperEx.createIndex(db, IDX_PUBLISH_TS_V2, TABLE_NAME_V2, COL_CATEGORY_ID); } @@ -149,6 +157,17 @@ static void addChannelPkColumn(SQLiteDatabase db) { SQLiteOpenHelperEx.addColumn(db, TABLE_NAME_V2, COL_CHANNEL_PK); } + static void removeChannelTitle(SQLiteDatabase db) { + final Column[] allColumns = getAllColumns(false); + + String columnList = Stream.of(allColumns).filter(it -> it != null).map(Column::name).collect(Collectors.joining(",")); + + SQLiteOpenHelperEx.updateTableSchema(db, TABLE_NAME_V2, SQLiteOpenHelperEx.getCreateTableCommand(TABLE_NAME_V2, allColumns), + "insert into " + TABLE_NAME_V2 + " (" + columnList + + ") select " + columnList + ); + } + static void addPublishTimeIndex(SQLiteDatabase db) { SQLiteOpenHelperEx.createIndex(db, IDX_PUBLISH_TIMESTAMP, TABLE_NAME_V2, COL_PUBLISH_TIME); } From bc984cadcea6e8aa1668de7863f1801e638f43e5 Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Fri, 9 Aug 2024 22:37:08 +0200 Subject: [PATCH 06/10] Upgrade Skytube-components --- app/build.gradle | 4 +- .../businessobjects/Sponsorblock/SBTasks.java | 8 - .../businessobjects/db/BookmarksDb.java | 11 +- .../db/CategoryManagement.java | 7 +- .../rm/skytube/businessobjects/db/Column.java | 32 ---- .../db/DownloadedVideosDb.java | 3 +- .../businessobjects/db/LocalChannelTable.java | 8 +- .../db/SQLiteOpenHelperEx.java | 142 ------------------ .../businessobjects/db/SubscriptionsDb.java | 72 ++++----- .../db/SubscriptionsTable.java | 11 +- .../db/SubscriptionsVideosTable.java | 39 ++--- 11 files changed, 81 insertions(+), 256 deletions(-) delete mode 100644 app/src/main/java/free/rm/skytube/businessobjects/db/Column.java diff --git a/app/build.gradle b/app/build.gradle index 40b936c659..63605c259a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -164,9 +164,11 @@ dependencies { // implementation 'com.github.TeamNewPipe.NewPipeExtractor:NewPipeExtractor:v0.22.6' implementation 'com.github.SkyTubeTeam.NewPipeExtractor:NewPipeExtractor:skytube-2024-07-24' - implementation ('com.github.SkyTubeTeam.components:okhttp-client:0.0.7') { + def skytubeComponents = '0.0.9' + implementation ("com.github.SkyTubeTeam.components:okhttp-client:$skytubeComponents") { exclude group: 'com.github.SkyTubeTeam.NewPipeExtractor', module: 'extractor' } + implementation "com.github.SkyTubeTeam.components:android-utils:$skytubeComponents" // Java HTML parser implementation "org.jsoup:jsoup:1.17.2" diff --git a/app/src/main/java/free/rm/skytube/businessobjects/Sponsorblock/SBTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/Sponsorblock/SBTasks.java index cc87387a38..154739d71a 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/Sponsorblock/SBTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/Sponsorblock/SBTasks.java @@ -9,19 +9,11 @@ import androidx.annotation.StringRes; import org.json.JSONArray; -import org.json.JSONException; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.net.HttpURLConnection; -import java.net.URL; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; -import free.rm.skytube.BuildConfig; import free.rm.skytube.R; import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeService; diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java index b3d5c98826..5183b3efd9 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java @@ -22,11 +22,11 @@ import android.database.Cursor; import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; -import android.view.Menu; import androidx.annotation.NonNull; import androidx.core.util.Pair; +import com.github.skytube.components.utils.SQLiteHelper; import com.google.gson.Gson; import org.json.JSONException; @@ -39,17 +39,14 @@ import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.app.Utils; -import free.rm.skytube.app.utils.WeakList; import free.rm.skytube.businessobjects.Logger; import free.rm.skytube.businessobjects.YouTube.POJOs.CardData; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeVideo; import free.rm.skytube.businessobjects.YouTube.newpipe.VideoId; -import free.rm.skytube.businessobjects.interfaces.CardListener; import free.rm.skytube.businessobjects.interfaces.OrderableDatabase; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Single; -import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; /** @@ -227,7 +224,7 @@ public void updateOrder(List videos) { */ public boolean isBookmarked(String videoId) { SkyTubeApp.nonUiThread(); - return executeQueryForInteger(BookmarksTable.IS_BOOKMARKED_QUERY, new String[]{videoId}, 0) > 0; + return SQLiteHelper.executeQueryForInteger(getReadableDatabase(), BookmarksTable.IS_BOOKMARKED_QUERY, new String[]{videoId}, 0) > 0; } /** @@ -235,7 +232,7 @@ public boolean isBookmarked(String videoId) { */ public Single getTotalBookmarkCount() { return Single.fromCallable(() -> - executeQueryForInteger(BookmarksTable.COUNT_ALL_BOOKMARKS, 0) + SQLiteHelper.executeQueryForInteger(getReadableDatabase(), BookmarksTable.COUNT_ALL_BOOKMARKS, 0) ).subscribeOn(Schedulers.io()); } @@ -244,7 +241,7 @@ public Single getTotalBookmarkCount() { */ public int getMaximumOrderNumber() { SkyTubeApp.nonUiThread(); - return executeQueryForInteger(BookmarksTable.MAXIMUM_ORDER_QUERY, 0); + return SQLiteHelper.executeQueryForInteger(getReadableDatabase(), BookmarksTable.MAXIMUM_ORDER_QUERY, 0); } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/CategoryManagement.java b/app/src/main/java/free/rm/skytube/businessobjects/db/CategoryManagement.java index 106eafe861..505f3600c6 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/CategoryManagement.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/CategoryManagement.java @@ -24,10 +24,9 @@ import androidx.annotation.StringRes; +import com.github.skytube.components.utils.SQLiteHelper; import com.mikepenz.iconics.typeface.library.materialdesigniconic.MaterialDesignIconic; -import org.apache.commons.codec.binary.StringUtils; - import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -109,11 +108,11 @@ public List getCategories() { } Category addNew(String name, MaterialDesignIconic.Icon icon, boolean builtin) { - int count = SQLiteOpenHelperEx.executeQueryForInteger(db, CategoriesTable.COUNT_BY_LABEL_QUERY, new String[] { name }, 0); + int count = SQLiteHelper.executeQueryForInteger(db, CategoriesTable.COUNT_BY_LABEL_QUERY, new String[] { name }, 0); if (count > 0) { return null; } - final int priority = SQLiteOpenHelperEx.executeQueryForInteger(db, CategoriesTable.MAXIMUM_PRIORITY_QUERY, 0) + 1; + final int priority = SQLiteHelper.executeQueryForInteger(db, CategoriesTable.MAXIMUM_PRIORITY_QUERY, 0) + 1; ContentValues values = new ContentValues(); values.put(CategoriesTable.COL_ENABLED, 1); diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/Column.java b/app/src/main/java/free/rm/skytube/businessobjects/db/Column.java deleted file mode 100644 index b85546a477..0000000000 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/Column.java +++ /dev/null @@ -1,32 +0,0 @@ -package free.rm.skytube.businessobjects.db; - -import android.database.Cursor; - -final class Column { - - final String name; - final String type; - final String modifier; - public Column(final String name, final String type) { - this.name = name; - this.type = type; - this.modifier = null; - } - - public Column(final String name, final String type, String modifier) { - this.name = name; - this.type = type; - this.modifier = modifier; - } - - public String format() { - return name + ' ' + type + (modifier != null ? " "+ modifier : ""); - } - - public String name() { - return name; - } - public int getColumn(Cursor cursor) { - return cursor.getColumnIndex(name); - } -} diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/DownloadedVideosDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/DownloadedVideosDb.java index fba19361b2..8084cd8b1f 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/DownloadedVideosDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/DownloadedVideosDb.java @@ -10,6 +10,7 @@ import androidx.annotation.NonNull; +import com.github.skytube.components.utils.SQLiteHelper; import com.google.gson.Gson; import org.json.JSONException; @@ -521,7 +522,7 @@ public void updateOrder(List videos) { */ public Single getTotalCount() { return Single.fromCallable(() -> - executeQueryForInteger(DownloadedVideosTable.COUNT_ALL, 0) + SQLiteHelper.executeQueryForInteger(getReadableDatabase(), DownloadedVideosTable.COUNT_ALL, 0) ).subscribeOn(Schedulers.io()); } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java index ce8624b0c8..77bd1a433b 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java @@ -19,6 +19,8 @@ import android.database.sqlite.SQLiteDatabase; +import com.github.skytube.components.utils.Column; +import com.github.skytube.components.utils.SQLiteHelper; import com.google.common.base.Joiner; import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; @@ -35,7 +37,7 @@ public class LocalChannelTable { public static final String COL_SUBSCRIBER_COUNT = "Subscriber_Count"; public static final Column COL_ID = new Column("_id", "integer", " primary key"); - static final String GET_ID_AND_CHANNEL_ID = String.format("SELECT %s, %s FROM %s", LocalChannelTable.COL_ID.name, LocalChannelTable.COL_CHANNEL_ID, LocalChannelTable.TABLE_NAME); + static final String GET_ID_AND_CHANNEL_ID = String.format("SELECT %s, %s FROM %s", LocalChannelTable.COL_ID.name(), LocalChannelTable.COL_CHANNEL_ID, LocalChannelTable.TABLE_NAME); private static final String[] ALL_COLUMNS = new String[]{ LocalChannelTable.COL_CHANNEL_ID, @@ -63,12 +65,12 @@ public static String getCreateStatement(boolean withPk) { public static final void addIdColumn(SQLiteDatabase db) { final String allRowNames = Joiner.on(',').join(ALL_COLUMNS); - SQLiteOpenHelperEx.updateTableSchema(db, TABLE_NAME, getCreateStatement(true), + SQLiteHelper.updateTableSchema(db, TABLE_NAME, getCreateStatement(true), "insert into " + TABLE_NAME + " (_id," + allRowNames + ") select rowid," + allRowNames); } public static void updateLatestVideoTimestamp(SQLiteDatabase db, PersistentChannel persistentChannel, long latestPublishTimestamp) { - db.execSQL("update " + TABLE_NAME + " set " + COL_LAST_VIDEO_TS + " = max(?, coalesce(" + COL_LAST_VIDEO_TS + ",0)) where " + COL_ID.name + " = ?", new Object[]{ + db.execSQL("update " + TABLE_NAME + " set " + COL_LAST_VIDEO_TS + " = max(?, coalesce(" + COL_LAST_VIDEO_TS + ",0)) where " + COL_ID.name() + " = ?", new Object[]{ latestPublishTimestamp, persistentChannel.subscriptionPk()}); } } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java index 4077216392..7113094bb0 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java @@ -18,15 +18,11 @@ package free.rm.skytube.businessobjects.db; import android.content.Context; -import android.database.Cursor; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteException; import android.database.sqlite.SQLiteOpenHelper; import java.io.File; import free.rm.skytube.app.SkyTubeApp; -import free.rm.skytube.businessobjects.Logger; /** * An extended {@link SQLiteOpenHelper} with extra goodies. @@ -39,17 +35,6 @@ public SQLiteOpenHelperEx(Context context, String name, android.database.sqlite. super(context, name, factory, version); } - -// /** -// * Closes the database and clears any singleton instances. -// */ -// @Override -// public synchronized void close() { -// super.close(); -// clearDatabaseInstance(); -// } - - /** * @return The database (full) path. */ @@ -65,131 +50,4 @@ public File getDatabaseDirectory() { return SkyTubeApp.getContext().getDatabasePath(getDatabaseName()).getParentFile(); } - /** - * Execute a constant query, and return the number in the first row, first column. - * - * @param query the query to execute - * @param selectionArgs the arguments for the query. - * @return a number. - */ - public Integer executeQueryForInteger(String query, String[] selectionArgs, Integer defaultValue) { - return executeQueryForInteger(getReadableDatabase(), query, selectionArgs, defaultValue); - } - - /** - * Execute a constant query, and return the number in the first row, first column. - * - * @param query the query to execute - * @return a number. - */ - public Integer executeQueryForInteger(String query, Integer defaultValue) { - return executeQueryForInteger(getReadableDatabase(), query, null, defaultValue); - } - - /** - * Execute a constant query, and return the number in the first row, first column. - * - * @param db the database to execute on. - * @param query the query to execute - * @param selectionArgs the arguments for the query. - * @return a number. - */ - public static Integer executeQueryForInteger(SQLiteDatabase db, String query, String[] selectionArgs, Integer defaultValue) { - try (Cursor cursor = db.rawQuery(query, selectionArgs)) { - if (cursor.moveToFirst()) { - return cursor.getInt(0); - } - } - return defaultValue; - } - - /** - * Execute a constant query, and return the number in the first row, first column. - * - * @param query the query to execute - * @return a number. - */ - public static Integer executeQueryForInteger(SQLiteDatabase db, String query, Integer defaultValue) { - return executeQueryForInteger(db, query, null, defaultValue); - } - - public Long getOptionalLong(Cursor cursor, String columnName) { - int columnIndex = cursor.getColumnIndexOrThrow(columnName); - return cursor.isNull(columnIndex) ? null : cursor.getLong(columnIndex); - } - - public Long getLong(Cursor cursor, String columnName) { - return cursor.getLong(cursor.getColumnIndexOrThrow(columnName)); - } - - /** - * Execute the given sql updates, one-by-one. Throws an exception if any of them fails. - */ - public static void execSQLUpdates(SQLiteDatabase db, String[] sqlUpdates) { - for (String sqlUpdate : sqlUpdates) { - db.execSQL(sqlUpdate); - } - } - - public static void continueOnError(SQLiteDatabase db, String update) { - try { - db.execSQL(update); - } catch (SQLiteException e) { - Logger.e(db, e,"Unable to execute %s , because: %s", update, e.getMessage()); - } - } - - public static void addColumn(SQLiteDatabase db, String tableName, Column column) { - try { - db.execSQL("ALTER TABLE " + tableName + " ADD COLUMN " + column.format()); - } catch (SQLiteException e) { - Logger.e(db, e,"Unable to add '%s' '%s' to table: '%s', because: %s", column.name, column.type, tableName, e.getMessage()); - } - } - - public static void createIndex(SQLiteDatabase db, String indexName, String tableName, Column... columns) { - db.execSQL("CREATE INDEX "+indexName+" ON "+tableName + "("+listColumns(true, columns)+")"); - } - - public static String getCreateTableCommand(String tableName, Column... columns) { - return "CREATE TABLE " + tableName + " (" + listColumns(false, columns) + ")"; - } - - public static void createTable(SQLiteDatabase db, String tableName, Column... columns) { - try { - db.execSQL(getCreateTableCommand(tableName, columns)); - } catch (SQLiteException e) { - Logger.e(db, e,"Unable to create table: '%s', because: %s", tableName, e.getMessage()); - throw e; - } - } - - public static void updateTableSchema(SQLiteDatabase db, String tableName, String newTableCreateStatement, String migration) { - db.execSQL("ALTER TABLE " + tableName + " RENAME TO old_" + tableName); - db.execSQL(newTableCreateStatement); - db.execSQL(migration + " from old_" + tableName); - db.execSQL("DROP TABLE old_" + tableName); - } - - private static String listColumns(boolean justNames, final Column[] columns) { - boolean first = true; - final StringBuilder sql = new StringBuilder(); - for (Column col : columns) { - if (col == null) { - continue; - } - if (first) { - first = false; - } else { - sql.append(", "); - } - if (justNames) { - sql.append(col.name); - } else { - sql.append(col.format()); - } - } - return sql.toString(); - } - } \ No newline at end of file diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index b31aad6923..d4c040a174 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java @@ -23,6 +23,8 @@ import android.database.SQLException; import android.database.sqlite.SQLiteDatabase; +import com.github.skytube.components.utils.SQLiteHelper; +import com.github.skytube.components.utils.Stopwatch; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; @@ -62,7 +64,7 @@ public class SubscriptionsDb extends SQLiteOpenHelperEx { private static final String HAS_VIDEO_QUERY = String.format("SELECT COUNT(*) FROM %s WHERE %s = ?", SubscriptionsVideosTable.TABLE_NAME_V2, SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID); private static final String GET_VIDEO_IDS_BY_CHANNEL_TO_PUBLISH_TS = String.format("SELECT %s,%s FROM %s WHERE %s = ?", - SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, SubscriptionsVideosTable.COL_PUBLISH_TIME.name, SubscriptionsVideosTable.TABLE_NAME_V2, SubscriptionsVideosTable.COL_CHANNEL_ID); + SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, SubscriptionsVideosTable.COL_PUBLISH_TIME.name(), SubscriptionsVideosTable.TABLE_NAME_V2, SubscriptionsVideosTable.COL_CHANNEL_ID); private static final String GET_VIDEO_IDS_BY_CHANNEL = String.format("SELECT %s FROM %s WHERE %s = ?", SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, SubscriptionsVideosTable.TABLE_NAME_V2, SubscriptionsVideosTable.COL_CHANNEL_ID); private static final String FIND_EMPTY_RETRIEVAL_TS = String.format("SELECT %s,%s FROM %s WHERE %s IS NULL", @@ -113,14 +115,14 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(SubscriptionsVideosTable.getCreateStatement()); } if (upgrade.executeStep(3)) { - execSQLUpdates(db, SubscriptionsTable.getAddColumns()); + SQLiteHelper.execSQLUpdates(db, SubscriptionsTable.getAddColumns()); } if (upgrade.executeStep(4)) { - execSQLUpdates(db, SubscriptionsVideosTable.getAddTimestampColumns()); + SQLiteHelper.execSQLUpdates(db, SubscriptionsVideosTable.getAddTimestampColumns()); setupRetrievalTimestamp(db); } if (upgrade.executeStep(5)) { - execSQLUpdates(db, SubscriptionsTable.getLastCheckTimeColumn()); + SQLiteHelper.execSQLUpdates(db, SubscriptionsTable.getLastCheckTimeColumn()); db.execSQL(LocalChannelTable.getCreateStatement(false)); try { for (YouTubeChannel channel : getSubscribedChannels(db)) { @@ -140,7 +142,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL(SubscriptionsVideosTable.getIndexOnVideos()); } if (upgrade.executeStep(8)) { - continueOnError(db, CategoriesTable.getCreateStatement()); + SQLiteHelper.continueOnError(db, CategoriesTable.getCreateStatement()); new CategoryManagement(db).setupDefaultCategories(); SubscriptionsTable.addCategoryColumn(db); } @@ -209,7 +211,7 @@ private static Map getChannelIdLongMap(final SQLiteDatabase db, private void dropOldSubsVideosTable(final SQLiteDatabase db) { Logger.w(this, "Dropping old Subs table"); - continueOnError(db, SubscriptionsVideosTable.getDropTableStatement()); + SQLiteHelper.continueOnError(db, SubscriptionsVideosTable.getDropTableStatement()); } private void fixChannelIds(final SQLiteDatabase db) { @@ -345,7 +347,7 @@ public DatabaseResult unsubscribe(PersistentChannel channel) { // delete any feed videos pertaining to this channel getWritableDatabase().delete(SubscriptionsVideosTable.TABLE_NAME_V2, - SubscriptionsVideosTable.COL_SUBS_ID.name + " = ?", + SubscriptionsVideosTable.COL_SUBS_ID.name() + " = ?", toArray(channel.channelPk())); // remove this channel from the subscriptions DB @@ -407,39 +409,39 @@ private ContentValues convertToContentValues(final YouTubeVideo video, @Nullable ContentValues values = new ContentValues(); if (persistentChannel != null) { ChannelId chId = persistentChannel.channel().getChannelId(); - values.put(SubscriptionsVideosTable.COL_CHANNEL_ID_V2.name, chId.getRawId()); - values.put(SubscriptionsVideosTable.COL_SUBS_ID.name, persistentChannel.subscriptionPk()); - values.put(SubscriptionsVideosTable.COL_CHANNEL_PK.name, persistentChannel.channelPk()); + values.put(SubscriptionsVideosTable.COL_CHANNEL_ID_V2.name(), chId.getRawId()); + values.put(SubscriptionsVideosTable.COL_SUBS_ID.name(), persistentChannel.subscriptionPk()); + values.put(SubscriptionsVideosTable.COL_CHANNEL_PK.name(), persistentChannel.channelPk()); } - values.put(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID_V2.name, video.getId()); - values.put(SubscriptionsVideosTable.COL_CATEGORY_ID.name, video.getCategoryId()); - values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME_EXACT.name, video.getPublishTimestampExact()); - values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name, video.getPublishTimestamp()); + values.put(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID_V2.name(), video.getId()); + values.put(SubscriptionsVideosTable.COL_CATEGORY_ID.name(), video.getCategoryId()); + values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME_EXACT.name(), video.getPublishTimestampExact()); + values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name(), video.getPublishTimestamp()); if (video.getLikeCountNumber() != null) { - values.put(SubscriptionsVideosTable.COL_LIKES.name, video.getLikeCountNumber()); + values.put(SubscriptionsVideosTable.COL_LIKES.name(), video.getLikeCountNumber()); } if (video.getDislikeCountNumber() != null) { - values.put(SubscriptionsVideosTable.COL_DISLIKES.name, video.getDislikeCountNumber()); + values.put(SubscriptionsVideosTable.COL_DISLIKES.name(), video.getDislikeCountNumber()); } if (video.getViewsCountInt() != null) { - values.put(SubscriptionsVideosTable.COL_VIEWS.name, video.getViewsCountInt().longValue()); + values.put(SubscriptionsVideosTable.COL_VIEWS.name(), video.getViewsCountInt().longValue()); } if (video.getTitle() != null) { - values.put(SubscriptionsVideosTable.COL_TITLE.name, video.getTitle()); + values.put(SubscriptionsVideosTable.COL_TITLE.name(), video.getTitle()); } if (video.getDescription() != null) { - values.put(SubscriptionsVideosTable.COL_DESCRIPTION.name, video.getDescription()); + values.put(SubscriptionsVideosTable.COL_DESCRIPTION.name(), video.getDescription()); } - values.put(SubscriptionsVideosTable.COL_DURATION.name, video.getDurationInSeconds()); + values.put(SubscriptionsVideosTable.COL_DURATION.name(), video.getDurationInSeconds()); if (video.getThumbnailUrl() != null) { - values.put(SubscriptionsVideosTable.COL_THUMBNAIL_URL.name, video.getThumbnailUrl()); + values.put(SubscriptionsVideosTable.COL_THUMBNAIL_URL.name(), video.getThumbnailUrl()); } return values; } public int setPublishTimestamp(YouTubeVideo video) { ContentValues values = new ContentValues(); - values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name, video.getPublishTimestamp()); + values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name(), video.getPublishTimestamp()); return getWritableDatabase().update( SubscriptionsVideosTable.TABLE_NAME_V2, @@ -487,7 +489,7 @@ private List getSubscribedChannels(SQLiteDatabase db) throws IOE final int colSubscribers = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_SUBSCRIBER_COUNT); final int colLastCheck = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_LAST_CHECK_TS); final int colLastVisit = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_LAST_VISIT_TIME); - final int colCategoryId = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CATEGORY_ID.name); + final int colCategoryId = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CATEGORY_ID.name()); do { final String id = cursor.getString(colChannelIdNum); @@ -517,7 +519,7 @@ private Integer getInteger(Cursor cursor, int colCategoryId) { public boolean isUserSubscribedToChannel(ChannelId channelId) { SkyTubeApp.nonUiThread(); - return executeQueryForInteger(IS_SUBSCRIBED_QUERY, toArrayParam(channelId), 0) > 0; + return SQLiteHelper.executeQueryForInteger(getReadableDatabase(), IS_SUBSCRIBED_QUERY, toArrayParam(channelId), 0) > 0; } public Single getUserSubscribedToChannel(ChannelId channelId) { @@ -573,7 +575,7 @@ public long updateLastVideoFetch(ChannelId channelId) { } private boolean hasVideo(YouTubeVideo video) { - return executeQueryForInteger(HAS_VIDEO_QUERY, new String[]{video.getId()}, 0) > 0; + return SQLiteHelper.executeQueryForInteger(getReadableDatabase(), HAS_VIDEO_QUERY, new String[]{video.getId()}, 0) > 0; } /** @@ -611,8 +613,8 @@ private ContentValues createContentValues(YouTubeVideo video, PersistentChannel ContentValues values = convertToContentValues(video, persistentChannel); values.put(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, video.getId()); final long publishInstant = video.getPublishTimestamp(); - values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name, publishInstant); - values.put(SubscriptionsVideosTable.COL_CATEGORY_ID.name, video.getCategoryId()); + values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name(), publishInstant); + values.put(SubscriptionsVideosTable.COL_CATEGORY_ID.name(), video.getCategoryId()); return values; } @@ -625,7 +627,7 @@ public List getSubscriptionVideoPage(int limit, String videoId, lo SkyTubeApp.nonUiThread(); final String selection; - final String sortingColumn = SubscriptionsVideosTable.COL_PUBLISH_TIME.name; + final String sortingColumn = SubscriptionsVideosTable.COL_PUBLISH_TIME.name(); final String[] selectionArguments; if (videoId != null) { selection = "WHERE (" + sortingColumn + " < ?) OR (" + sortingColumn + " = ? AND " + SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID + " > ?)"; @@ -657,7 +659,7 @@ private List extractVideos(Cursor cursor, boolean fullColumnList) if (cursor.moveToNext()) { final int idIdx = cursor.getColumnIndex(SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID); - final int categoryIdx = cursor.getColumnIndex(SubscriptionsVideosTable.COL_CATEGORY_ID.name); + final int categoryIdx = cursor.getColumnIndex(SubscriptionsVideosTable.COL_CATEGORY_ID.name()); final int publishTsIdx = fullColumnList ? SubscriptionsVideosTable.COL_PUBLISH_TIME.getColumn(cursor) : -1; final int publishTsExactIdx = SubscriptionsVideosTable.COL_PUBLISH_TIME_EXACT.getColumn(cursor); final int titleColumn = SubscriptionsVideosTable.COL_TITLE.getColumn(cursor); @@ -718,17 +720,17 @@ public PersistentChannel getCachedChannel(ChannelId channelId) { "select s._id subs_id, c.* from Channel c left outer Join Subs s on c.Channel_Id = s.Channel_Id where c.Channel_Id = ?", toArrayParam(channelId))) { if (cursor.moveToNext()) { - Long subscriptionPk = getOptionalLong(cursor, "subs_id"); - Long channelPk = getLong(cursor, LocalChannelTable.COL_ID.name); + Long subscriptionPk = SQLiteHelper.getOptionalLong(cursor, "subs_id"); + Long channelPk = SQLiteHelper.getLong(cursor, LocalChannelTable.COL_ID.name()); String title = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_TITLE)); String description = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_DESCRIPTION)); String thumbnail = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_THUMBNAIL_NORMAL_URL)); String banner = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_BANNER_URL)); - long subscriberCount = getLong(cursor, LocalChannelTable.COL_SUBSCRIBER_COUNT); - long lastCheckTs = getLong(cursor, LocalChannelTable.COL_LAST_CHECK_TS); + long subscriberCount = SQLiteHelper.getLong(cursor, LocalChannelTable.COL_SUBSCRIBER_COUNT); + long lastCheckTs = SQLiteHelper.getLong(cursor, LocalChannelTable.COL_LAST_CHECK_TS); // TODO: use - Long lastVideoTs = getOptionalLong(cursor, LocalChannelTable.COL_LAST_VIDEO_TS); + Long lastVideoTs = SQLiteHelper.getOptionalLong(cursor, LocalChannelTable.COL_LAST_VIDEO_TS); YouTubeChannel channel = new YouTubeChannel(channelId.getRawId(), title, description, thumbnail, banner, subscriberCount, subscriptionPk != null, -1, lastCheckTs, null, Collections.emptyList()); return new PersistentChannel(channel, channelPk, subscriptionPk); } @@ -781,7 +783,7 @@ private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentCh int count = db.update( LocalChannelTable.TABLE_NAME, values, - LocalChannelTable.COL_ID.name + " = ?", + LocalChannelTable.COL_ID.name() + " = ?", toArray(channelPk)); if (count != 1) { throw new IllegalStateException("Unable to update channel " + channel + ", with pk= " + channelPk); diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java index 55cfc24cbc..8abef18e5d 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java @@ -19,6 +19,9 @@ import android.database.sqlite.SQLiteDatabase; +import com.github.skytube.components.utils.Column; +import com.github.skytube.components.utils.SQLiteHelper; + import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; /** @@ -64,7 +67,7 @@ public static String[] getAddColumns() { } public static void addCategoryColumn(SQLiteDatabase db) { - SQLiteOpenHelperEx.addColumn(db, TABLE_NAME, COL_CATEGORY_ID); + SQLiteHelper.addColumn(db, TABLE_NAME, COL_CATEGORY_ID); } public static String[] getLastCheckTimeColumn() { @@ -72,9 +75,9 @@ public static String[] getLastCheckTimeColumn() { } public static void cleanupTable(SQLiteDatabase db) { - SQLiteOpenHelperEx.updateTableSchema(db, TABLE_NAME, getCreateStatement(), - "insert into " + TABLE_NAME + " (_id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_VIDEO_FETCH + - ") select _id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_CHECK_TIME ); + SQLiteHelper.updateTableSchema(db, TABLE_NAME, getCreateStatement(), + "insert into " + TABLE_NAME + " (_id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name() + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_VIDEO_FETCH + + ") select _id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name() + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_CHECK_TIME ); } public static void updateLastVideoFetchTimestamps(SQLiteDatabase db, PersistentChannel persistentChannel) { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java index 265f23d9ad..047d141d02 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsVideosTable.java @@ -19,7 +19,8 @@ import android.database.sqlite.SQLiteDatabase; -import com.google.common.base.Joiner; +import com.github.skytube.components.utils.Column; +import com.github.skytube.components.utils.SQLiteHelper; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -64,18 +65,18 @@ public class SubscriptionsVideosTable { private static final String IDX_PUBLISH_TIMESTAMP = "IDX_subscription_videos_PublishTime"; static final String[] ALL_COLUMNS_FOR_EXTRACT = new String[] { - COL_CHANNEL_ID_V2.name, - COL_YOUTUBE_VIDEO_ID_V2.name, - COL_CATEGORY_ID.name, - COL_TITLE.name, - COL_DESCRIPTION.name, - COL_THUMBNAIL_URL.name, - COL_LIKES.name, - COL_DISLIKES.name, - COL_VIEWS.name, - COL_DURATION.name, - COL_PUBLISH_TIME.name, - COL_PUBLISH_TIME_EXACT.name, + COL_CHANNEL_ID_V2.name(), + COL_YOUTUBE_VIDEO_ID_V2.name(), + COL_CATEGORY_ID.name(), + COL_TITLE.name(), + COL_DESCRIPTION.name(), + COL_THUMBNAIL_URL.name(), + COL_LIKES.name(), + COL_DISLIKES.name(), + COL_VIEWS.name(), + COL_DURATION.name(), + COL_PUBLISH_TIME.name(), + COL_PUBLISH_TIME_EXACT.name(), }; static final String BASE_QUERY; @@ -145,16 +146,16 @@ private static Column[] getAllColumns(boolean withChannelTitle) { } public static void addNewFlatTable(SQLiteDatabase db, boolean withChannelTitle) { - db.execSQL(SQLiteOpenHelperEx.getCreateTableCommand(TABLE_NAME_V2, getAllColumns(withChannelTitle))); - SQLiteOpenHelperEx.createIndex(db, IDX_PUBLISH_TS_V2, TABLE_NAME_V2, COL_CATEGORY_ID); + db.execSQL(SQLiteHelper.getCreateTableCommand(TABLE_NAME_V2, getAllColumns(withChannelTitle))); + SQLiteHelper.createIndex(db, IDX_PUBLISH_TS_V2, TABLE_NAME_V2, COL_CATEGORY_ID); } static void addSubsIdColumn(SQLiteDatabase db) { - SQLiteOpenHelperEx.addColumn(db, TABLE_NAME_V2, COL_SUBS_ID); + SQLiteHelper.addColumn(db, TABLE_NAME_V2, COL_SUBS_ID); } static void addChannelPkColumn(SQLiteDatabase db) { - SQLiteOpenHelperEx.addColumn(db, TABLE_NAME_V2, COL_CHANNEL_PK); + SQLiteHelper.addColumn(db, TABLE_NAME_V2, COL_CHANNEL_PK); } static void removeChannelTitle(SQLiteDatabase db) { @@ -162,13 +163,13 @@ static void removeChannelTitle(SQLiteDatabase db) { String columnList = Stream.of(allColumns).filter(it -> it != null).map(Column::name).collect(Collectors.joining(",")); - SQLiteOpenHelperEx.updateTableSchema(db, TABLE_NAME_V2, SQLiteOpenHelperEx.getCreateTableCommand(TABLE_NAME_V2, allColumns), + SQLiteHelper.updateTableSchema(db, TABLE_NAME_V2, SQLiteHelper.getCreateTableCommand(TABLE_NAME_V2, allColumns), "insert into " + TABLE_NAME_V2 + " (" + columnList + ") select " + columnList ); } static void addPublishTimeIndex(SQLiteDatabase db) { - SQLiteOpenHelperEx.createIndex(db, IDX_PUBLISH_TIMESTAMP, TABLE_NAME_V2, COL_PUBLISH_TIME); + SQLiteHelper.createIndex(db, IDX_PUBLISH_TIMESTAMP, TABLE_NAME_V2, COL_PUBLISH_TIME); } } From d303dcea316df83c45e7fd4f89b58161d23e4823 Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Sat, 10 Aug 2024 00:00:27 +0200 Subject: [PATCH 07/10] Fix unsubscribe to delete old videos --- .../YouTube/POJOs/YouTubeChannel.java | 33 ------------------- .../businessobjects/YouTube/YouTubeTasks.java | 9 ----- .../YouTube/newpipe/NewPipeService.java | 1 - .../businessobjects/db/DatabaseTasks.java | 7 +++- .../businessobjects/db/SubscriptionsDb.java | 4 +-- .../views/ChannelActionHandler.java | 2 +- .../views/SubscribeButton.java | 4 --- 7 files changed, 9 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java index 3edc701841..67651aac97 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/YouTubeChannel.java @@ -286,39 +286,6 @@ private Single removeDeniedChannel(boolean displayToastMessage) { }); } - public static Disposable subscribeChannel(final Context context, final ChannelId channelId) { - if (channelId != null) { - return DatabaseTasks.getChannelInfo(context, channelId, false) - .observeOn(Schedulers.io()) - .map(persistentChannel -> - new Pair<>(persistentChannel, SubscriptionsDb.getSubscriptionsDb().subscribe(persistentChannel, Collections.emptyList())) - ) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(youTubeChannelWithResult -> { - switch(youTubeChannelWithResult.second) { - case SUCCESS: { - youTubeChannelWithResult.first.channel.setUserSubscribed(true); - EventBus.getInstance().notifyMainTabChanged(EventBus.SettingChange.SUBSCRIPTION_LIST_CHANGED); - SkyTubeApp.getSettings().setRefreshSubsFeedFromCache(true); - Toast.makeText(context, R.string.channel_subscribed, Toast.LENGTH_LONG).show(); - break; - } - case NOT_MODIFIED: { - Toast.makeText(context, R.string.channel_already_subscribed, Toast.LENGTH_LONG).show(); - break; - } - default: { - Toast.makeText(context, R.string.channel_subscribe_failed, Toast.LENGTH_LONG).show(); - break; - } - } - }); - } else { - Toast.makeText(context, "Channel is not specified", Toast.LENGTH_LONG).show(); - return Disposable.empty(); - } - } - public String getChannelUrl() { return getChannelId().toURL(); } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java index a9b2b3d85e..fb1f4db49e 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java @@ -89,15 +89,6 @@ public static Single refreshAllSubscriptions(Context context, @Nullable }); } - public static Single refreshSubscribedChannel(ChannelId channelId, @Nullable Consumer newVideosFound) { - if (SkyTubeApp.getSettings().isUseNewPipe() || !YouTubeAPIKey.get().isUserApiKeySet()) { - return YouTubeTasks.getBulkSubscriptionVideos(Collections.singletonList(channelId), newVideosFound); - } else { - return YouTubeTasks.getChannelVideos(channelId, null, false, newVideosFound) - .map(items -> items.size()); - } - } - private static Single refreshSubscriptions(@NonNull List channelIds, @Nullable Consumer newVideosFound) { if (SkyTubeApp.getSettings().isUseNewPipe() || !YouTubeAPIKey.get().isUserApiKeySet()) { return YouTubeTasks.getBulkSubscriptionVideos(channelIds, newVideosFound); diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java index 8c73bd0f39..69c137983d 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java @@ -322,7 +322,6 @@ public PersistentChannel getChannelDetails(ChannelId channelId, PersistentChanne Logger.i(this, "Fetching channel details for " + channelId); VideoPagerWithChannel pager = getChannelPager(channelId.getRawId()); // get the channel, and add all the videos from the first page - YouTubeChannel channel = pager.getChannel(); try { return pager.getNextPageAsVideosAndUpdateChannel(persistentChannel); } catch (NewPipeException e) { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java index ed0ee533e2..c951a325e4 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java @@ -166,7 +166,12 @@ public static Single> subscribeToChannel return Single.fromCallable(() -> { PersistentChannel channel = DatabaseTasks.getChannelOrRefresh(context, channelId, true); SubscriptionsDb db = SubscriptionsDb.getSubscriptionsDb(); - DatabaseResult result = subscribeToChannel ? db.subscribe(channel, Collections.emptyList()) : db.unsubscribe(channel); + final DatabaseResult result; + if (subscribeToChannel) { + result = db.subscribe(channel, channel.channel().getYouTubeVideos()); + } else { + result = db.unsubscribe(channel); + } return Pair.create(channel, result); }) .subscribeOn(Schedulers.io()) diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index d4c040a174..2f07e9e1f8 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java @@ -344,11 +344,11 @@ private DatabaseResult saveSubscription(ChannelId channelId) { */ public DatabaseResult unsubscribe(PersistentChannel channel) { SkyTubeApp.nonUiThread(); - + Logger.i(this, "unsubscribing subs_id= %s, channel_id = %s, channel_pk = %s", channel.subscriptionPk(), channel.channel().getChannelId(), channel.channelPk()); // delete any feed videos pertaining to this channel getWritableDatabase().delete(SubscriptionsVideosTable.TABLE_NAME_V2, SubscriptionsVideosTable.COL_SUBS_ID.name() + " = ?", - toArray(channel.channelPk())); + toArray(channel.subscriptionPk())); // remove this channel from the subscriptions DB int rowsDeleted = getWritableDatabase().delete(SubscriptionsTable.TABLE_NAME, diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java index 939d8cb499..14539f2833 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java @@ -41,7 +41,7 @@ public ChannelActionHandler(CompositeDisposable compositeDisposable) { public boolean handleChannelActions(Context context, YouTubeChannel channel, int itemId) { switch (itemId) { case R.id.subscribe_channel: - compositeDisposable.add(YouTubeChannel.subscribeChannel(context, channel.getChannelId())); + compositeDisposable.add(DatabaseTasks.subscribeToChannel(true, null, context, channel.getChannelId(), true).subscribe()); return true; case R.id.unsubscribe_channel: compositeDisposable.add(DatabaseTasks.subscribeToChannel(false, diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/views/SubscribeButton.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/SubscribeButton.java index f642eb0958..cb0528951f 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/views/SubscribeButton.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/SubscribeButton.java @@ -59,10 +59,6 @@ public void onClick(View view) { externalClickListener.onClick(SubscribeButton.this); } if(channel != null) { - // Only fetch videos for this channel if fetchChannelVideosOnSubscribe is true AND the channel is not subscribed to yet. - if (!isUserSubscribed) { - compositeDisposable.add(YouTubeTasks.refreshSubscribedChannel(channel.getChannelId(), null).subscribe()); - } compositeDisposable.add(DatabaseTasks.subscribeToChannel(!isUserSubscribed, this, getContext(), channel.getChannelId(), true).subscribe()); } From 7d7d8b7a96735d8d0cbd2f706a4c79e9981fcc7e Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Sat, 10 Aug 2024 23:28:06 +0200 Subject: [PATCH 08/10] Better sqlite handling --- .../businessobjects/FileDownloader.java | 7 ++- .../businessobjects/db/BookmarksDb.java | 37 +++++++-------- .../businessobjects/db/PlaybackStatusDb.java | 33 ++++++------- .../businessobjects/db/SearchHistoryDb.java | 47 ++++++++++--------- .../adapters/SearchHistoryCursorAdapter.java | 2 +- 5 files changed, 64 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/free/rm/skytube/businessobjects/FileDownloader.java b/app/src/main/java/free/rm/skytube/businessobjects/FileDownloader.java index dcd10c4599..dac3d90c17 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/FileDownloader.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/FileDownloader.java @@ -201,13 +201,12 @@ private void fileDownloadStatus() { .query(new DownloadManager.Query().setFilterById(downloadId)); if (cursor != null && cursor.moveToFirst()) { - final int columnIndex = cursor.getColumnIndex(DownloadManager.COLUMN_STATUS); - downloadSuccessful = (cursor.getInt(columnIndex) == DownloadManager.STATUS_SUCCESSFUL); + downloadSuccessful = (cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_SUCCESSFUL); if (downloadSuccessful) { - downloadedFileUri = Uri.parse(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))); + downloadedFileUri = Uri.parse(cursor.getString(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI))); } else { - final int columnReason = cursor.getColumnIndex(DownloadManager.COLUMN_REASON); + final int columnReason = cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_REASON); final int reason = cursor.getInt(columnReason); // output why the download has failed... diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java index 5183b3efd9..e8e84a706d 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java @@ -167,29 +167,26 @@ private DatabaseResult remove(VideoId video) { if (rowsDeleted > 0) { // Since we've removed a video, we will need to update the order column for all the videos. int order = 1; - Cursor cursor = getReadableDatabase().query( + try (Cursor cursor = getReadableDatabase().query( BookmarksTable.TABLE_NAME, new String[]{BookmarksTable.COL_YOUTUBE_VIDEO, BookmarksTable.COL_ORDER}, null, - null, null, null, BookmarksTable.COL_ORDER + " ASC"); - if (cursor.moveToNext()) { - Gson gson = new Gson(); - do { - byte[] blob = cursor.getBlob(cursor.getColumnIndex(BookmarksTable.COL_YOUTUBE_VIDEO)); - YouTubeVideo uvideo = gson.fromJson(new String(blob), YouTubeVideo.class).updatePublishTimestampFromDate(); - ContentValues contentValues = new ContentValues(); - contentValues.put(BookmarksTable.COL_ORDER, order++); - - getWritableDatabase().update(BookmarksTable.TABLE_NAME, contentValues, BookmarksTable.COL_YOUTUBE_VIDEO_ID + " = ?", - new String[]{uvideo.getId()}); - } while (cursor.moveToNext()); - } - - cursor.close(); - - return DatabaseResult.SUCCESS; - } - return DatabaseResult.NOT_MODIFIED; + null, null, null, BookmarksTable.COL_ORDER + " ASC")) { + int blobCol = cursor.getColumnIndexOrThrow(BookmarksTable.COL_YOUTUBE_VIDEO); + Gson gson = new Gson(); + while (cursor.moveToNext()) { + byte[] blob = cursor.getBlob(blobCol); + YouTubeVideo uvideo = gson.fromJson(new String(blob), YouTubeVideo.class).updatePublishTimestampFromDate(); + ContentValues contentValues = new ContentValues(); + contentValues.put(BookmarksTable.COL_ORDER, order++); + + getWritableDatabase().update(BookmarksTable.TABLE_NAME, contentValues, BookmarksTable.COL_YOUTUBE_VIDEO_ID + " = ?", + new String[]{uvideo.getId()}); + } + } + return DatabaseResult.SUCCESS; + } + return DatabaseResult.NOT_MODIFIED; } catch (SQLException e) { Logger.e(this, "Database error: " + e.getMessage(), e); return DatabaseResult.ERROR; diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/PlaybackStatusDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/PlaybackStatusDb.java index 909d05272f..070383e513 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/PlaybackStatusDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/PlaybackStatusDb.java @@ -75,22 +75,23 @@ public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) { */ public synchronized VideoWatchedStatus getVideoWatchedStatus(@NonNull String videoId) { if(playbackHistoryMap == null) { - Cursor cursor = getReadableDatabase().query( - PlaybackStatusTable.TABLE_NAME, - new String[]{PlaybackStatusTable.COL_YOUTUBE_VIDEO_ID, PlaybackStatusTable.COL_YOUTUBE_VIDEO_POSITION, PlaybackStatusTable.COL_YOUTUBE_VIDEO_WATCHED}, - null, - null, null, null, null); - playbackHistoryMap = new HashMap<>(); - if(cursor.moveToFirst()) { - do { - String video_id = cursor.getString(cursor.getColumnIndex(PlaybackStatusTable.COL_YOUTUBE_VIDEO_ID)); - int position = cursor.getInt(cursor.getColumnIndex(PlaybackStatusTable.COL_YOUTUBE_VIDEO_POSITION)); - int finished = cursor.getInt(cursor.getColumnIndex(PlaybackStatusTable.COL_YOUTUBE_VIDEO_WATCHED)); - VideoWatchedStatus status = new VideoWatchedStatus(position, finished == 1); - playbackHistoryMap.put(video_id, status); - } while (cursor.moveToNext()); - } - cursor.close(); + try (Cursor cursor = getReadableDatabase().query( + PlaybackStatusTable.TABLE_NAME, + new String[]{PlaybackStatusTable.COL_YOUTUBE_VIDEO_ID, PlaybackStatusTable.COL_YOUTUBE_VIDEO_POSITION, PlaybackStatusTable.COL_YOUTUBE_VIDEO_WATCHED}, + null, + null, null, null, null)) { + playbackHistoryMap = new HashMap<>(); + final int videoIdIdx = cursor.getColumnIndexOrThrow(PlaybackStatusTable.COL_YOUTUBE_VIDEO_ID); + final int positionIdx = cursor.getColumnIndexOrThrow(PlaybackStatusTable.COL_YOUTUBE_VIDEO_POSITION); + final int finishedIdx = cursor.getColumnIndexOrThrow(PlaybackStatusTable.COL_YOUTUBE_VIDEO_WATCHED); + while (cursor.moveToNext()) { + String video_id = cursor.getString(videoIdIdx); + int position = cursor.getInt(positionIdx); + int finished = cursor.getInt(finishedIdx); + VideoWatchedStatus status = new VideoWatchedStatus(position, finished == 1); + playbackHistoryMap.put(video_id, status); + } + } } if(playbackHistoryMap.get(videoId) == null) { // Requested video has no entry in the database, so create one in the Map. No need to create it in the Database yet - if needed, diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SearchHistoryDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SearchHistoryDb.java index c1bcb44807..6edf2ce0fb 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SearchHistoryDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SearchHistoryDb.java @@ -59,32 +59,37 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // grab all the values from the database, recreate it with the new column, and add the search terms back in. // The current timestamp will be used. if(oldVersion == 1 && newVersion == 2) { - Cursor cursor = db.query(SearchHistoryTable.TABLE_NAME, + try (Cursor cursor = db.query(SearchHistoryTable.TABLE_NAME, new String[] {SearchHistoryTable.COL_SEARCH_ID, SearchHistoryTable.COL_SEARCH_TEXT}, null, null, null, null, - SearchHistoryTable.COL_SEARCH_ID + " ASC"); - List> history = new ArrayList<>(); - if(cursor.moveToFirst()) { - do { - int id = cursor.getInt(cursor.getColumnIndex(SearchHistoryTable.COL_SEARCH_ID)); - String text = cursor.getString(cursor.getColumnIndex(SearchHistoryTable.COL_SEARCH_TEXT)); - history.add(new HashMap(){{put(id, text); }}); - } while (cursor.moveToNext()); - db.execSQL("DROP TABLE " + SearchHistoryTable.TABLE_NAME); - onCreate(db); - for(Map entry : history) { - for(Map.Entry e : entry.entrySet()) { - String text = e.getValue(); - ContentValues values = new ContentValues(); - values.put(SearchHistoryTable.COL_SEARCH_ID, e.getKey()); - values.put(SearchHistoryTable.COL_SEARCH_TEXT, text); - db.insert(SearchHistoryTable.TABLE_NAME, null, values); - } - } - } + SearchHistoryTable.COL_SEARCH_ID + " ASC")) { + List> history = new ArrayList<>(); + if (cursor.moveToFirst()) { + final int searchIdIdx = cursor.getColumnIndexOrThrow(SearchHistoryTable.COL_SEARCH_ID); + final int searchTxtIdx = cursor.getColumnIndexOrThrow(SearchHistoryTable.COL_SEARCH_TEXT); + do { + int id = cursor.getInt(searchIdIdx); + String text = cursor.getString(searchTxtIdx); + history.add(new HashMap() {{ + put(id, text); + }}); + } while (cursor.moveToNext()); + db.execSQL("DROP TABLE " + SearchHistoryTable.TABLE_NAME); + onCreate(db); + for (Map entry : history) { + for (Map.Entry e : entry.entrySet()) { + String text = e.getValue(); + ContentValues values = new ContentValues(); + values.put(SearchHistoryTable.COL_SEARCH_ID, e.getKey()); + values.put(SearchHistoryTable.COL_SEARCH_TEXT, text); + db.insert(SearchHistoryTable.TABLE_NAME, null, values); + } + } + } + } } } diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SearchHistoryCursorAdapter.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SearchHistoryCursorAdapter.java index 4c30971eed..8ee24000e9 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SearchHistoryCursorAdapter.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SearchHistoryCursorAdapter.java @@ -61,7 +61,7 @@ public void setSearchBarString(String searchBarString) { @Override public CharSequence convertToString(Cursor cursor) { - int indexColumnSuggestion = cursor.getColumnIndex(SearchHistoryTable.COL_SEARCH_TEXT); + int indexColumnSuggestion = cursor.getColumnIndexOrThrow(SearchHistoryTable.COL_SEARCH_TEXT); return cursor.getString(indexColumnSuggestion); } From 804af1ad70e7ec31335590226375d17231fa562c Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Sun, 11 Aug 2024 19:10:40 +0200 Subject: [PATCH 09/10] Add 'state' and indexes to the subscription table --- .../businessobjects/db/LocalChannelTable.java | 19 ++++++++++++---- .../businessobjects/db/SubscriptionsDb.java | 22 +++++++++++++------ .../db/SubscriptionsTable.java | 9 ++++++++ 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java index 77bd1a433b..9d9b6286e6 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java @@ -27,7 +27,7 @@ public class LocalChannelTable { public static final String TABLE_NAME = "Channel"; - public static final String COL_CHANNEL_ID = "Channel_Id"; + public static final String COL_CHANNEL_ID_name = "Channel_Id"; public static final String COL_LAST_VIDEO_TS = "Last_Video_TS"; public static final String COL_LAST_CHECK_TS = "Last_Check_TS"; public static final String COL_TITLE = "Title"; @@ -36,11 +36,13 @@ public class LocalChannelTable { public static final String COL_BANNER_URL = "Banner_Url"; public static final String COL_SUBSCRIBER_COUNT = "Subscriber_Count"; public static final Column COL_ID = new Column("_id", "integer", " primary key"); + public static final Column COL_CHANNEL_ID = new Column(COL_CHANNEL_ID_name, "text", "UNIQUE NOT NULL"); + public static final Column COL_STATE = new Column("state", "integer", "default 0"); - static final String GET_ID_AND_CHANNEL_ID = String.format("SELECT %s, %s FROM %s", LocalChannelTable.COL_ID.name(), LocalChannelTable.COL_CHANNEL_ID, LocalChannelTable.TABLE_NAME); + static final String GET_ID_AND_CHANNEL_ID = String.format("SELECT %s, %s FROM %s", LocalChannelTable.COL_ID.name(), LocalChannelTable.COL_CHANNEL_ID.name(), LocalChannelTable.TABLE_NAME); private static final String[] ALL_COLUMNS = new String[]{ - LocalChannelTable.COL_CHANNEL_ID, + LocalChannelTable.COL_CHANNEL_ID.name(), LocalChannelTable.COL_TITLE, LocalChannelTable.COL_DESCRIPTION, LocalChannelTable.COL_BANNER_URL, @@ -52,7 +54,8 @@ public class LocalChannelTable { public static String getCreateStatement(boolean withPk) { return "CREATE TABLE " + TABLE_NAME + " (" + (withPk ? COL_ID.format() + "," : "") + - COL_CHANNEL_ID + " TEXT UNIQUE NOT NULL, " + + COL_CHANNEL_ID.format() + ", " + + COL_STATE.format() + ", " + COL_TITLE + " TEXT, " + COL_DESCRIPTION + " TEXT, " + COL_THUMBNAIL_NORMAL_URL+ " TEXT, " + @@ -73,4 +76,12 @@ public static void updateLatestVideoTimestamp(SQLiteDatabase db, PersistentChann db.execSQL("update " + TABLE_NAME + " set " + COL_LAST_VIDEO_TS + " = max(?, coalesce(" + COL_LAST_VIDEO_TS + ",0)) where " + COL_ID.name() + " = ?", new Object[]{ latestPublishTimestamp, persistentChannel.subscriptionPk()}); } + + public static void addChannelIdIndex(SQLiteDatabase db) { + SQLiteHelper.createIndex(db, "IDX_channel_channelId", TABLE_NAME, COL_CHANNEL_ID); + } + + public static void addStateColumn(SQLiteDatabase db) { + SQLiteHelper.addColumn(db, TABLE_NAME, COL_STATE); + } } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index 2f07e9e1f8..28eb8c9760 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java @@ -70,7 +70,7 @@ public class SubscriptionsDb extends SQLiteOpenHelperEx { private static final String FIND_EMPTY_RETRIEVAL_TS = String.format("SELECT %s,%s FROM %s WHERE %s IS NULL", SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, SubscriptionsVideosTable.COL_YOUTUBE_VIDEO, SubscriptionsVideosTable.TABLE_NAME, SubscriptionsVideosTable.COL_RETRIEVAL_TS); - private static final String SUBSCRIBED_CHANNEL_INFO = "SELECT c.Channel_Id,c.Title,c.Thumbnail_Normal_Url,s.Last_Visit_Time,(select max(publish_time) from subscription_videos videos where videos.Channel_Id = s.Channel_Id) as latest_video_ts FROM Subs s,Channel c where s.Channel_Id = c.Channel_Id "; + private static final String SUBSCRIBED_CHANNEL_INFO = "SELECT c.Channel_Id,c.Title,c.Thumbnail_Normal_Url,s.Last_Visit_Time,c.Last_Video_TS as latest_video_ts FROM Subs s,Channel c where s.channel_pk = c._Id "; private static final String SUBSCRIBED_CHANNEL_INFO_ORDER_BY = " ORDER BY LOWER(" + LocalChannelTable.COL_TITLE + ") ASC"; private static final String SUBSCRIBED_CHANNEL_LIMIT_BY_TITLE = " and LOWER(c." +LocalChannelTable.COL_TITLE + ") like ?"; @@ -81,7 +81,7 @@ public class SubscriptionsDb extends SQLiteOpenHelperEx { private static volatile SubscriptionsDb subscriptionsDb = null; - private static final int DATABASE_VERSION = 17; + private static final int DATABASE_VERSION = 18; private static final String DATABASE_NAME = "subs.db"; @@ -103,6 +103,7 @@ public void onCreate(SQLiteDatabase db) { SubscriptionsVideosTable.addNewFlatTable(db, false); SubscriptionsVideosTable.addPublishTimeIndex(db); db.execSQL(LocalChannelTable.getCreateStatement(true)); + LocalChannelTable.addChannelIdIndex(db); db.execSQL(CategoriesTable.getCreateStatement()); new CategoryManagement(db).setupDefaultCategories(); } @@ -173,6 +174,12 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { SubscriptionsVideosTable.removeChannelTitle(db); SubscriptionsVideosTable.addPublishTimeIndex(db); } + if (upgrade.executeStep(18)) { + Logger.w(this, "Optimize Channel table"); + LocalChannelTable.addChannelIdIndex(db); + SubscriptionsTable.addChannelIdColumn(db); + LocalChannelTable.addStateColumn(db); + } } private void normalizeSubscriptionVideosTable(final SQLiteDatabase db) { @@ -300,7 +307,7 @@ public DatabaseResult subscribe(PersistentChannel persistentChannel, Collection< SkyTubeApp.nonUiThread(); saveChannelVideos(videos, persistentChannel, false); - return saveSubscription(persistentChannel.channel().getChannelId()); + return saveSubscription(persistentChannel.channelPk(), persistentChannel.channel().getChannelId()); } /** @@ -310,11 +317,12 @@ public DatabaseResult subscribe(PersistentChannel persistentChannel, Collection< * * @return True if the operation was successful; false otherwise. */ - private DatabaseResult saveSubscription(ChannelId channelId) { + private DatabaseResult saveSubscription(long channelPk, ChannelId channelId) { SkyTubeApp.nonUiThread(); ContentValues values = new ContentValues(); values.put(SubscriptionsTable.COL_CHANNEL_ID, channelId.getRawId()); + values.put(SubscriptionsTable.COL_CHANNEL_PK.name(), channelPk); SQLiteDatabase db = getWritableDatabase(); try { @@ -481,7 +489,7 @@ private List getSubscribedChannels(SQLiteDatabase db) throws IOE List subsChannels = new ArrayList<>(); if (cursor.moveToNext()) { - final int colChannelIdNum = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_CHANNEL_ID); + final int colChannelIdNum = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_CHANNEL_ID.name()); final int colTitle = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_TITLE); final int colDescription = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_DESCRIPTION); final int colBanner = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_BANNER_URL); @@ -790,7 +798,7 @@ private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentCh } return new PersistentChannel(channel, channelPk, subPk); } - values.put(LocalChannelTable.COL_CHANNEL_ID, channel.getChannelId().getRawId()); + values.put(LocalChannelTable.COL_CHANNEL_ID.name(), channel.getChannelId().getRawId()); long newPk = db.insert(LocalChannelTable.TABLE_NAME, null, values); return new PersistentChannel(channel, newPk, subPk); } @@ -821,7 +829,7 @@ private static ContentValues toContentValues(YouTubeChannel channel) { public List getSubscribedChannelsByText(String searchText, boolean sortChannelsAlphabetically) { List result = new ArrayList<>(); - try (Cursor cursor = createSubscriptionCursor(searchText, sortChannelsAlphabetically)) { + try (Cursor cursor = createSubscriptionCursor(searchText, sortChannelsAlphabetically); Stopwatch s = new Stopwatch("search for "+searchText)) { final int channelId = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CHANNEL_ID); final int title = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_TITLE); final int thumbnail = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_THUMBNAIL_NORMAL_URL); diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java index 8abef18e5d..600073ce7a 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java @@ -41,6 +41,7 @@ public class SubscriptionsTable { private static final String COL_SUBSCRIBER_COUNT = "Subscriber_Count"; public static final Column COL_CATEGORY_ID = new Column("category_id", "INTEGER"); public static final String COL_LAST_VIDEO_FETCH = "last_video_fetch_time"; + public static final Column COL_CHANNEL_PK = new Column("channel_pk", "integer"); private static final String ADD_COLUMN = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN "; @@ -50,6 +51,7 @@ public static String getCreateStatement() { return "CREATE TABLE " + TABLE_NAME + " (" + COL_ID + " INTEGER PRIMARY KEY ASC, " + COL_CHANNEL_ID + " TEXT UNIQUE NOT NULL, " + + COL_CHANNEL_PK.format() + ", " + COL_CATEGORY_ID.format() + ", " + COL_LAST_VISIT_TIME + " TIMESTAMP DEFAULT (strftime('%s', 'now')), " + COL_LAST_VIDEO_FETCH + " INTEGER " + @@ -86,4 +88,11 @@ public static void updateLastVideoFetchTimestamps(SQLiteDatabase db, PersistentC System.currentTimeMillis(), persistentChannel.subscriptionPk() }); } } + + public static void addChannelIdColumn(SQLiteDatabase db) { + SQLiteHelper.addColumn(db, TABLE_NAME, COL_CHANNEL_PK); + db.execSQL("update " + TABLE_NAME + + " set " + COL_CHANNEL_PK.name() + + " = (select " + LocalChannelTable.COL_ID.name() + " from " + LocalChannelTable.TABLE_NAME + " c where c." + LocalChannelTable.COL_CHANNEL_ID_name + " = " + TABLE_NAME + '.' + COL_CHANNEL_ID + ")"); + } } From 5d67a23e7850a400d5074037b7b0d34e40252bab Mon Sep 17 00:00:00 2001 From: Zsombor Gegesy Date: Sun, 18 Aug 2024 13:58:17 +0200 Subject: [PATCH 10/10] Update channel status, when YT reports as "terminated"/"deleted" Display the status in the subs drawer, and do not fetch videos for channels with not-ok state --- .../YouTube/NewPipeChannelVideos.java | 5 +- .../YouTube/POJOs/ChannelView.java | 9 ++- .../YouTube/POJOs/PersistentChannel.java | 23 +++++-- .../businessobjects/YouTube/YouTubeTasks.java | 18 ++++- .../newpipe/GetPlaylistsForChannel.java | 2 +- .../YouTube/newpipe/NewPipeService.java | 14 ++-- .../businessobjects/db/DatabaseTasks.java | 11 ++- .../businessobjects/db/LocalChannelTable.java | 10 +++ .../businessobjects/db/SubscriptionsDb.java | 68 ++++++++++--------- .../db/SubscriptionsTable.java | 6 ++ .../skytube/businessobjects/model/Status.java | 34 ++++++++++ .../SubscriptionsBackupsManager.java | 4 -- .../businessobjects/adapters/SubsAdapter.java | 20 ++++-- app/src/main/res/values/strings_subs.xml | 3 + 14 files changed, 169 insertions(+), 58 deletions(-) create mode 100644 app/src/main/java/free/rm/skytube/businessobjects/model/Status.java diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/NewPipeChannelVideos.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/NewPipeChannelVideos.java index e1311b727f..19ba89aa7e 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/NewPipeChannelVideos.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/NewPipeChannelVideos.java @@ -18,6 +18,7 @@ import java.util.Objects; +import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeException; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeService; import free.rm.skytube.businessobjects.YouTube.newpipe.VideoPager; @@ -27,11 +28,11 @@ */ public class NewPipeChannelVideos extends NewPipeVideos { - private String channelId; + private ChannelId channelId; // Important, this is called from the channel tab public void setQuery(String query) { - this.channelId = Objects.requireNonNull(query, "query missing"); + this.channelId = new ChannelId(query); } @Override diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/ChannelView.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/ChannelView.java index b13133e2b0..4c6d51c301 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/ChannelView.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/ChannelView.java @@ -17,18 +17,21 @@ package free.rm.skytube.businessobjects.YouTube.POJOs; import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; +import free.rm.skytube.businessobjects.model.Status; public class ChannelView { private final ChannelId id; private final String title; private final String thumbnailUrl; private boolean newVideosSinceLastVisit; + private final Status status; - public ChannelView(ChannelId id, String title, String thumbnailUrl, boolean newVideosSinceLastVisit) { + public ChannelView(ChannelId id, String title, String thumbnailUrl, boolean newVideosSinceLastVisit, Status status) { this.id = id; this.title = title; this.thumbnailUrl = thumbnailUrl; this.newVideosSinceLastVisit = newVideosSinceLastVisit; + this.status = status; } public ChannelId getId() { @@ -47,6 +50,10 @@ public boolean isNewVideosSinceLastVisit() { return newVideosSinceLastVisit; } + public Status status() { + return status; + } + public void setNewVideosSinceLastVisit(boolean newVideosSinceLastVisit) { this.newVideosSinceLastVisit = newVideosSinceLastVisit; } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java index 8b65aa9d0f..821e6c766d 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java @@ -16,21 +16,36 @@ */ package free.rm.skytube.businessobjects.YouTube.POJOs; +import java.util.Objects; + +import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; +import free.rm.skytube.businessobjects.model.Status; + public final class PersistentChannel { final YouTubeChannel channel; final long channelPk; final Long subscriptionPk; + final Status status; - public PersistentChannel(YouTubeChannel channel, long channelPk, Long subscriptionPk) { - this.channel = channel; + public PersistentChannel(YouTubeChannel channel, long channelPk, Long subscriptionPk, Status status) { + this.channel = Objects.requireNonNull(channel, "channel"); this.channelPk = channelPk; this.subscriptionPk = subscriptionPk; + this.status = status; } public YouTubeChannel channel() { return channel; } + public Status status() { + return status; + } + + public ChannelId getChannelId() { + return channel.getChannelId(); + } + public long channelPk() { return channelPk; } @@ -45,10 +60,10 @@ public boolean isSubscribed() { public PersistentChannel with(YouTubeChannel newInstance) { newInstance.setUserSubscribed(isSubscribed()); - return new PersistentChannel(newInstance, channelPk, subscriptionPk); + return new PersistentChannel(newInstance, channelPk, subscriptionPk, status); } public PersistentChannel withSubscriptionPk(Long newSubscriptionPk) { - return new PersistentChannel(channel, channelPk, newSubscriptionPk); + return new PersistentChannel(channel, channelPk, newSubscriptionPk, status); } } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java index fb1f4db49e..da4da40b7d 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/YouTubeTasks.java @@ -10,6 +10,8 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; import org.schabi.newpipe.extractor.StreamingService; +import org.schabi.newpipe.extractor.exceptions.AccountTerminatedException; +import org.schabi.newpipe.extractor.exceptions.ContentNotAvailableException; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ReCaptchaException; @@ -42,8 +44,10 @@ import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeException; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeService; import free.rm.skytube.businessobjects.YouTube.newpipe.PlaylistPager; +import free.rm.skytube.businessobjects.db.LocalChannelTable; import free.rm.skytube.businessobjects.db.SubscriptionsDb; import free.rm.skytube.businessobjects.interfaces.GetDesiredStreamListener; +import free.rm.skytube.businessobjects.model.Status; import free.rm.skytube.gui.businessobjects.adapters.PlaylistsGridAdapter; import free.rm.skytube.gui.businessobjects.adapters.VideoGridAdapter; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; @@ -203,11 +207,23 @@ private static List fetchVideos(@NonNull SubscriptionsDb subscript }); return videos; } catch (NewPipeException e) { - Log.e(TAG, "Error during fetching channel page for " + channelId + ",msg:" + e.getMessage(), e); + handleNewPipeException(channelId, e); return Collections.emptyList(); } } + private static void handleNewPipeException(@NonNull ChannelId channelId, @NonNull NewPipeException e) { + if (e.getCause() instanceof AccountTerminatedException) { + Log.e(TAG, "Account terminated for "+ channelId +" error: "+e.getMessage(), e); + SubscriptionsDb.getSubscriptionsDb().setChannelState(channelId, Status.ACCOUNT_TERMINATED); + } else if (e.getCause() instanceof ContentNotAvailableException) { + Log.e(TAG, "Channel doesn't exists "+ channelId +" error: "+e.getMessage(), e); + SubscriptionsDb.getSubscriptionsDb().setChannelState(channelId, Status.NOT_EXISTS); + } else { + Log.e(TAG, "Error during fetching channel page for " + channelId + ",msg:" + e.getMessage(), e); + } + } + /** * Task to asynchronously get videos for a specific channel. */ diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/GetPlaylistsForChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/GetPlaylistsForChannel.java index 482739b92f..98c1afc2b9 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/GetPlaylistsForChannel.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/GetPlaylistsForChannel.java @@ -104,7 +104,7 @@ public List getNextPlaylists() throws IOException, ExtractionEx private synchronized Paging getPaging() throws NewPipeException, ParsingException { SkyTubeApp.nonUiThread(); if (paging == null) { - NewPipeService.ChannelWithExtractor cwe = NewPipeService.get().getChannelWithExtractor(channel.getId()); + NewPipeService.ChannelWithExtractor cwe = NewPipeService.get().getChannelWithExtractor(channel.getChannelId()); paging = new Paging(cwe.channel, cwe.findPlaylistTab()); } return paging; diff --git a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java index 69c137983d..62d52cfe4e 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/NewPipeService.java @@ -209,7 +209,7 @@ public StreamInfo getStreamInfoByVideoId(String videoId) throws ExtractionExcept * @throws ExtractionException * @throws IOException */ - private List getChannelVideos(String channelId) throws NewPipeException { + private List getChannelVideos(ChannelId channelId) throws NewPipeException { SkyTubeApp.nonUiThread(); VideoPagerWithChannel pager = getChannelPager(channelId); List result = pager.getNextPageAsVideosAndUpdateChannel(null).channel().getYouTubeVideos(); @@ -255,7 +255,7 @@ public List getVideosFromFeedOrFromChannel(ChannelId channelId) th } catch (IOException | ExtractionException | RuntimeException | NewPipeException e) { Logger.e(this, "Unable to get videos from a feed " + channelId + " : "+ e.getMessage(), e); } - return getChannelVideos(channelId.getRawId()); + return getChannelVideos(channelId); } public VideoPager getTrending() throws NewPipeException { @@ -269,7 +269,7 @@ public VideoPager getTrending() throws NewPipeException { } } - public VideoPagerWithChannel getChannelPager(String channelId) throws NewPipeException { + public VideoPagerWithChannel getChannelPager(ChannelId channelId) throws NewPipeException { try { Logger.e(this, "fetching channel info: "+ channelId); ChannelWithExtractor channelExtractor = getChannelWithExtractor(channelId); @@ -279,7 +279,7 @@ public VideoPagerWithChannel getChannelPager(String channelId) throws NewPipeExc } } - public ChannelWithExtractor getChannelWithExtractor(String channelId) throws NewPipeException { + public ChannelWithExtractor getChannelWithExtractor(ChannelId channelId) throws NewPipeException { try { ChannelExtractor channelExtractor = getChannelExtractor(channelId); @@ -320,7 +320,7 @@ public CommentPager getCommentPager(String videoId) throws NewPipeException { */ public PersistentChannel getChannelDetails(ChannelId channelId, PersistentChannel persistentChannel) throws NewPipeException { Logger.i(this, "Fetching channel details for " + channelId); - VideoPagerWithChannel pager = getChannelPager(channelId.getRawId()); + VideoPagerWithChannel pager = getChannelPager(channelId); // get the channel, and add all the videos from the first page try { return pager.getNextPageAsVideosAndUpdateChannel(persistentChannel); @@ -359,11 +359,11 @@ private X callParser(ParserCall parser, X defaultValue) { } } - private ChannelExtractor getChannelExtractor(String channelId) + private ChannelExtractor getChannelExtractor(ChannelId channelId) throws ExtractionException, IOException { // Extract from it ChannelExtractor channelExtractor = streamingService - .getChannelExtractor(getListLinkHandler(Objects.requireNonNull(channelId, "channelId"))); + .getChannelExtractor(getListLinkHandler(Objects.requireNonNull(channelId, "channelId").getRawId())); channelExtractor.fetchPage(); return channelExtractor; } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java index c951a325e4..8e0006f8e8 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/DatabaseTasks.java @@ -26,6 +26,7 @@ import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeException; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeService; +import free.rm.skytube.businessobjects.model.Status; import free.rm.skytube.gui.businessobjects.views.ChannelSubscriber; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.core.Completable; @@ -79,7 +80,15 @@ public static PersistentChannel getChannelOrRefresh(Context context, ChannelId c needsRefresh = persistentChannel.channel().getLastCheckTime() < System.currentTimeMillis() - (24 * 60 * 60 * 1000L); } if (needsRefresh && SkyTubeApp.isConnected(context)) { - return NewPipeService.get().getChannelDetails(channelId, persistentChannel); + try { + return NewPipeService.get().getChannelDetails(channelId, persistentChannel); + } catch (NewPipeException newPipeException) { + if (persistentChannel != null && persistentChannel.status() != Status.OK) { + Log.e(TAG, "Channel is blocked/terminated - and kept that way: "+ persistentChannel+", message:"+newPipeException.getMessage()); + return persistentChannel; + } + throw newPipeException; + } } return persistentChannel; } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java index 9d9b6286e6..67e277364f 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/LocalChannelTable.java @@ -19,13 +19,18 @@ import android.database.sqlite.SQLiteDatabase; +import androidx.annotation.NonNull; + import com.github.skytube.components.utils.Column; import com.github.skytube.components.utils.SQLiteHelper; import com.google.common.base.Joiner; import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; +import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; +import free.rm.skytube.businessobjects.model.Status; public class LocalChannelTable { + public static final String TABLE_NAME = "Channel"; public static final String COL_CHANNEL_ID_name = "Channel_Id"; public static final String COL_LAST_VIDEO_TS = "Last_Video_TS"; @@ -77,6 +82,11 @@ public static void updateLatestVideoTimestamp(SQLiteDatabase db, PersistentChann latestPublishTimestamp, persistentChannel.subscriptionPk()}); } + public static void updateChannelStatus(SQLiteDatabase db, @NonNull ChannelId channelId, @NonNull Status status) { + db.execSQL("update " + TABLE_NAME + " set " + COL_STATE.name() + " = ? where " + COL_CHANNEL_ID.name() + " = ?", + new Object[] { status.code, channelId.getRawId() }); + } + public static void addChannelIdIndex(SQLiteDatabase db) { SQLiteHelper.createIndex(db, "IDX_channel_channelId", TABLE_NAME, COL_CHANNEL_ID); } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index 28eb8c9760..9302ebd9ae 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java @@ -44,6 +44,7 @@ import java.util.Objects; import java.util.Set; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import free.rm.skytube.app.SkyTubeApp; @@ -54,6 +55,7 @@ import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeVideo; import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; +import free.rm.skytube.businessobjects.model.Status; import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -70,11 +72,11 @@ public class SubscriptionsDb extends SQLiteOpenHelperEx { private static final String FIND_EMPTY_RETRIEVAL_TS = String.format("SELECT %s,%s FROM %s WHERE %s IS NULL", SubscriptionsVideosTable.COL_YOUTUBE_VIDEO_ID, SubscriptionsVideosTable.COL_YOUTUBE_VIDEO, SubscriptionsVideosTable.TABLE_NAME, SubscriptionsVideosTable.COL_RETRIEVAL_TS); - private static final String SUBSCRIBED_CHANNEL_INFO = "SELECT c.Channel_Id,c.Title,c.Thumbnail_Normal_Url,s.Last_Visit_Time,c.Last_Video_TS as latest_video_ts FROM Subs s,Channel c where s.channel_pk = c._Id "; + private static final String SUBSCRIBED_CHANNEL_INFO = "SELECT c.Channel_Id,c.Title,c.Thumbnail_Normal_Url,s.Last_Visit_Time,c.Last_Video_TS as latest_video_ts,c.state FROM Subs s,Channel c where s.channel_pk = c._Id "; private static final String SUBSCRIBED_CHANNEL_INFO_ORDER_BY = " ORDER BY LOWER(" + LocalChannelTable.COL_TITLE + ") ASC"; private static final String SUBSCRIBED_CHANNEL_LIMIT_BY_TITLE = " and LOWER(c." +LocalChannelTable.COL_TITLE + ") like ?"; - private static final String GET_ALL_SUBSCRIBED_CHANNEL_ID = "SELECT "+SubscriptionsTable.COL_CHANNEL_ID + " FROM "+SubscriptionsTable.TABLE_NAME; + private static final String GET_ALL_SUBSCRIBED_CHANNEL_ID = "SELECT s." + SubscriptionsTable.COL_CHANNEL_ID + " FROM " + SubscriptionsTable.TABLE_NAME + " s, " + LocalChannelTable.TABLE_NAME + " c where s.channel_pk = c._Id and c." + LocalChannelTable.COL_STATE.name() + " = 0"; private static final String IS_SUBSCRIBED_QUERY = String.format("SELECT EXISTS(SELECT %s FROM %s WHERE %s =?) AS VAL ", SubscriptionsTable.COL_ID, SubscriptionsTable.TABLE_NAME, SubscriptionsTable.COL_CHANNEL_ID); private static final String GET_PK_FROM_CHANNEL_ID = "SELECT " + SubscriptionsTable.COL_ID + " FROM " + SubscriptionsTable.TABLE_NAME + " WHERE " + SubscriptionsTable.COL_CHANNEL_ID + " = ?"; @@ -307,7 +309,7 @@ public DatabaseResult subscribe(PersistentChannel persistentChannel, Collection< SkyTubeApp.nonUiThread(); saveChannelVideos(videos, persistentChannel, false); - return saveSubscription(persistentChannel.channelPk(), persistentChannel.channel().getChannelId()); + return saveSubscription(persistentChannel.channelPk(), persistentChannel.getChannelId()); } /** @@ -352,7 +354,7 @@ private DatabaseResult saveSubscription(long channelPk, ChannelId channelId) { */ public DatabaseResult unsubscribe(PersistentChannel channel) { SkyTubeApp.nonUiThread(); - Logger.i(this, "unsubscribing subs_id= %s, channel_id = %s, channel_pk = %s", channel.subscriptionPk(), channel.channel().getChannelId(), channel.channelPk()); + Logger.i(this, "unsubscribing subs_id= %s, channel_id = %s, channel_pk = %s", channel.subscriptionPk(), channel.getChannelId(), channel.channelPk()); // delete any feed videos pertaining to this channel getWritableDatabase().delete(SubscriptionsVideosTable.TABLE_NAME_V2, SubscriptionsVideosTable.COL_SUBS_ID.name() + " = ?", @@ -416,7 +418,7 @@ public void updateVideo(YouTubeVideo video) { private ContentValues convertToContentValues(final YouTubeVideo video, @Nullable PersistentChannel persistentChannel) { ContentValues values = new ContentValues(); if (persistentChannel != null) { - ChannelId chId = persistentChannel.channel().getChannelId(); + ChannelId chId = persistentChannel.getChannelId(); values.put(SubscriptionsVideosTable.COL_CHANNEL_ID_V2.name(), chId.getRawId()); values.put(SubscriptionsVideosTable.COL_SUBS_ID.name(), persistentChannel.subscriptionPk()); values.put(SubscriptionsVideosTable.COL_CHANNEL_PK.name(), persistentChannel.channelPk()); @@ -447,6 +449,7 @@ private ContentValues convertToContentValues(final YouTubeVideo video, @Nullable } return values; } + public int setPublishTimestamp(YouTubeVideo video) { ContentValues values = new ContentValues(); values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name(), video.getPublishTimestamp()); @@ -474,7 +477,19 @@ public Single> getSubscribedChannelIdsAsync() { .subscribeOn(Schedulers.io()); } - /** + public void setChannelState(@NonNull PersistentChannel channel, @NonNull Status status) { + Logger.i(this, "Set channel %s pk=%s, id=%s state to %s", channel.channel().getTitle(), channel.channelPk(), channel.getChannelId(), status); + SkyTubeApp.nonUiThread(); + LocalChannelTable.updateChannelStatus(getWritableDatabase(), channel.getChannelId(), status); + } + + public void setChannelState(@NonNull ChannelId channelId, @NonNull Status status) { + Logger.i(this, "Set channel id=%s state to %s", channelId, status); + SkyTubeApp.nonUiThread(); + LocalChannelTable.updateChannelStatus(getWritableDatabase(), channelId, status); + } + + /** * Returns a list of channels that the user subscribed to, without accessing the network * * @@ -559,29 +574,6 @@ public Single updateLastVisitTimeAsync(ChannelId channelId) { }).subscribeOn(Schedulers.io()); } - /** - * Updates the given channel's last visit time. - * - * @param channelId Channel ID - * - * @return last visit time, if the update was successful; -1 otherwise. - */ - public long updateLastVideoFetch(ChannelId channelId) { - SQLiteDatabase db = getWritableDatabase(); - long currentTime = System.currentTimeMillis(); - - ContentValues values = new ContentValues(); - values.put(SubscriptionsTable.COL_LAST_VIDEO_FETCH, currentTime); - - int count = db.update( - SubscriptionsTable.TABLE_NAME, - values, - SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channelId.getRawId()}); - - return (count > 0 ? currentTime : -1); - } - private boolean hasVideo(YouTubeVideo video) { return SQLiteHelper.executeQueryForInteger(getReadableDatabase(), HAS_VIDEO_QUERY, new String[]{video.getId()}, 0) > 0; } @@ -737,10 +729,11 @@ public PersistentChannel getCachedChannel(ChannelId channelId) { String banner = cursor.getString(cursor.getColumnIndexOrThrow(LocalChannelTable.COL_BANNER_URL)); long subscriberCount = SQLiteHelper.getLong(cursor, LocalChannelTable.COL_SUBSCRIBER_COUNT); long lastCheckTs = SQLiteHelper.getLong(cursor, LocalChannelTable.COL_LAST_CHECK_TS); + Status statusCode = getStatusCode(cursor); // TODO: use Long lastVideoTs = SQLiteHelper.getOptionalLong(cursor, LocalChannelTable.COL_LAST_VIDEO_TS); YouTubeChannel channel = new YouTubeChannel(channelId.getRawId(), title, description, thumbnail, banner, subscriberCount, subscriptionPk != null, -1, lastCheckTs, null, Collections.emptyList()); - return new PersistentChannel(channel, channelPk, subscriptionPk); + return new PersistentChannel(channel, channelPk, subscriptionPk, statusCode); } } return null; @@ -757,6 +750,10 @@ public PersistentChannel cacheChannel(@Nullable PersistentChannel persistentChan return cacheChannel(db, persistentChannel, channel); } + private Status getStatusCode(Cursor cursor) { + return Status.lookup(SQLiteHelper.getLong(cursor, LocalChannelTable.COL_STATE.name())); + } + private String[] toArray(Object obj) { return new String[] { String.valueOf(obj)}; } @@ -778,10 +775,13 @@ private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentCh ContentValues values = toContentValues(channel); final Long channelPk; + final Status status; if (persistentChannel != null) { channelPk = persistentChannel.channelPk(); + status = persistentChannel.status(); } else { channelPk = getChannelPk(db, channel.getChannelId()); + status = Status.OK; } Long subPk = persistentChannel != null ? persistentChannel.subscriptionPk() : null; @@ -796,11 +796,11 @@ private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentCh if (count != 1) { throw new IllegalStateException("Unable to update channel " + channel + ", with pk= " + channelPk); } - return new PersistentChannel(channel, channelPk, subPk); + return new PersistentChannel(channel, channelPk, subPk, status); } values.put(LocalChannelTable.COL_CHANNEL_ID.name(), channel.getChannelId().getRawId()); long newPk = db.insert(LocalChannelTable.TABLE_NAME, null, values); - return new PersistentChannel(channel, newPk, subPk); + return new PersistentChannel(channel, newPk, subPk, status); } private static ContentValues toContentValues(YouTubeChannel channel) { @@ -835,11 +835,13 @@ public List getSubscribedChannelsByText(String searchText, boolean final int thumbnail = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_THUMBNAIL_NORMAL_URL); final int colLastVisit = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_LAST_VISIT_TIME); final int colLatestVideoTs = cursor.getColumnIndexOrThrow("latest_video_ts"); + final int colStatus = cursor.getColumnIndexOrThrow(LocalChannelTable.COL_STATE.name()); while(cursor.moveToNext()) { Long lastVisit = cursor.getLong(colLastVisit); Long latestVideoTs = cursor.getLong(colLatestVideoTs); boolean hasNew = (latestVideoTs != null && (lastVisit == null || latestVideoTs > lastVisit)); - result.add(new ChannelView(new ChannelId(cursor.getString(channelId)), cursor.getString(title), cursor.getString(thumbnail), hasNew)); + Status status = Status.lookup(cursor.getInt(colStatus)); + result.add(new ChannelView(new ChannelId(cursor.getString(channelId)), cursor.getString(title), cursor.getString(thumbnail), hasNew, status)); } return result; } diff --git a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java index 600073ce7a..fcbb25ed00 100644 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java +++ b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsTable.java @@ -82,6 +82,12 @@ public static void cleanupTable(SQLiteDatabase db) { ") select _id," + COL_CHANNEL_ID + "," + COL_CATEGORY_ID.name() + "," + COL_LAST_VISIT_TIME + "," + COL_LAST_CHECK_TIME ); } + /** + * Updates the given channel's last fetch time with the current timestamp. + * + * @param persistentChannel The channel + * + */ public static void updateLastVideoFetchTimestamps(SQLiteDatabase db, PersistentChannel persistentChannel) { if (persistentChannel.isSubscribed()) { db.execSQL("update " + TABLE_NAME + " set " + COL_LAST_VIDEO_FETCH + " = ? where " + COL_ID + " = ?", new Object[] { diff --git a/app/src/main/java/free/rm/skytube/businessobjects/model/Status.java b/app/src/main/java/free/rm/skytube/businessobjects/model/Status.java new file mode 100644 index 0000000000..d88da27ae0 --- /dev/null +++ b/app/src/main/java/free/rm/skytube/businessobjects/model/Status.java @@ -0,0 +1,34 @@ +package free.rm.skytube.businessobjects.model; + +public enum Status { + OK (0), + ACCOUNT_TERMINATED (1), + NOT_EXISTS (2); + + public final int code; + Status(int code) { + this.code = code; + } + + + public static Status lookup(Long code) { + if (code != null){ + return lookup(code.intValue()); + } else { + throw new IllegalArgumentException("Missing code: "+ code); + } + } + + public static Status lookup(int code) { + switch (code) { + case 0: + return OK; + case 1: + return ACCOUNT_TERMINATED; + case 2: + return NOT_EXISTS; + } + throw new IllegalArgumentException("Unknown code: " + code); + } +} + diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/SubscriptionsBackupsManager.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/SubscriptionsBackupsManager.java index 95f8840d7e..cb3bc48ac6 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/SubscriptionsBackupsManager.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/SubscriptionsBackupsManager.java @@ -52,18 +52,14 @@ import free.rm.skytube.app.SkyTubeApp; import free.rm.skytube.businessobjects.Logger; import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; -import free.rm.skytube.businessobjects.YouTube.POJOs.YouTubeChannel; -import free.rm.skytube.businessobjects.YouTube.YouTubeTasks; import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; import free.rm.skytube.businessobjects.YouTube.newpipe.ContentId; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeException; import free.rm.skytube.businessobjects.YouTube.newpipe.NewPipeService; -import free.rm.skytube.businessobjects.db.DatabaseResult; import free.rm.skytube.businessobjects.db.DatabaseTasks; import free.rm.skytube.businessobjects.db.SubscriptionsDb; import free.rm.skytube.gui.businessobjects.preferences.BackupDatabases; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; -import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.schedulers.Schedulers; diff --git a/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SubsAdapter.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SubsAdapter.java index 27c4170162..8b48adbc2c 100644 --- a/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SubsAdapter.java +++ b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SubsAdapter.java @@ -29,9 +29,7 @@ import com.bumptech.glide.Glide; import com.bumptech.glide.request.RequestOptions; -import java.util.HashSet; import java.util.Iterator; -import java.util.Set; import free.rm.skytube.R; import free.rm.skytube.app.EventBus; @@ -39,7 +37,6 @@ import free.rm.skytube.businessobjects.YouTube.newpipe.ChannelId; import free.rm.skytube.businessobjects.db.DatabaseTasks; import free.rm.skytube.databinding.SubChannelBinding; -import free.rm.skytube.gui.businessobjects.MainActivityListener; import io.reactivex.rxjava3.disposables.CompositeDisposable; /** @@ -174,7 +171,22 @@ void updateInfo(ChannelView channel) { .apply(new RequestOptions().placeholder(R.drawable.channel_thumbnail_default)) .into(binding.subChannelThumbnailImageView); - binding.subChannelNameTextView.setText(channel.getTitle()); + final String prefix; + switch (channel.status()) { + case ACCOUNT_TERMINATED: { + prefix = itemView.getContext().getString(R.string.status_account_terminated); + break; + } + case NOT_EXISTS: { + prefix = itemView.getContext().getString(R.string.status_not_exists); + break; + } + default: { + prefix = ""; + break; + } + } + binding.subChannelNameTextView.setText(prefix + channel.getTitle()); binding.subChannelNewVideosNotification.setVisibility(channel.isNewVideosSinceLastVisit() ? View.VISIBLE : View.INVISIBLE); this.channel = channel; } diff --git a/app/src/main/res/values/strings_subs.xml b/app/src/main/res/values/strings_subs.xml index 9f17b5d236..6959ffd0cb 100644 --- a/app/src/main/res/values/strings_subs.xml +++ b/app/src/main/res/values/strings_subs.xml @@ -30,4 +30,7 @@ Could not import your YouTube subscriptions. About + [Deleted] + [Terminated] +