diff --git a/app/build.gradle b/app/build.gradle index 40b936c65..63605c259 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/extra/java/free/rm/skytube/gui/fragments/ChromecastControllerFragment.java b/app/src/extra/java/free/rm/skytube/gui/fragments/ChromecastControllerFragment.java index bc398a5f4..3546dbd44 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/FeedUpdateTask.java b/app/src/main/java/free/rm/skytube/app/FeedUpdateTask.java index 095d791b6..908c788ed 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/app/SkyTubeApp.java b/app/src/main/java/free/rm/skytube/app/SkyTubeApp.java index 4e888f290..90ca0ec9e 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/FileDownloader.java b/app/src/main/java/free/rm/skytube/businessobjects/FileDownloader.java index dcd10c459..dac3d90c1 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/Sponsorblock/SBTasks.java b/app/src/main/java/free/rm/skytube/businessobjects/Sponsorblock/SBTasks.java index cc87387a3..154739d71 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/YouTube/NewPipeChannelVideos.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/NewPipeChannelVideos.java index e1311b727..19ba89aa7 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 b13133e2b..4c6d51c30 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 new file mode 100644 index 000000000..821e6c766 --- /dev/null +++ b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/POJOs/PersistentChannel.java @@ -0,0 +1,69 @@ +/* + * 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; + +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, 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; + } + + 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, status); + } + + public PersistentChannel withSubscriptionPk(Long newSubscriptionPk) { + return new PersistentChannel(channel, channelPk, newSubscriptionPk, status); + } +} 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 f3b0f2d26..67651aac9 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); @@ -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. @@ -292,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(youTubeChannel -> - new Pair<>(youTubeChannel, SubscriptionsDb.getSubscriptionsDb().subscribe(youTubeChannel)) - ) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe(youTubeChannelWithResult -> { - switch(youTubeChannelWithResult.second) { - case SUCCESS: { - youTubeChannelWithResult.first.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/VideoBlocker.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/VideoBlocker.java index 7df7d5ae8..4f18dd28f 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 073ed1d45..da4da40b7 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; @@ -32,6 +34,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; @@ -41,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; @@ -88,15 +93,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); @@ -148,7 +144,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 +153,7 @@ private static Single getBulkSubscriptionVideos(@NonNull List getBulkSubscriptionVideos(@NonNull 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. */ @@ -243,7 +251,8 @@ private static Single> 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 +266,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() ); } @@ -406,19 +415,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 { @@ -427,21 +437,29 @@ 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); } } - SubscriptionsDb.getSubscriptionsDb().saveChannelVideos(channel.getYouTubeVideos(), channel.getChannelId()); + SubscriptionsDb db = SubscriptionsDb.getSubscriptionsDb(); + 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/businessobjects/YouTube/newpipe/GetPlaylistsForChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/GetPlaylistsForChannel.java index 3edeebd4c..98c1afc2b 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 @@ -16,15 +16,12 @@ */ package free.rm.skytube.businessobjects.YouTube.newpipe; -import android.util.Log; - import androidx.annotation.Nullable; import org.schabi.newpipe.extractor.InfoItem; import org.schabi.newpipe.extractor.ListExtractor; import org.schabi.newpipe.extractor.Page; import org.schabi.newpipe.extractor.channel.tabs.ChannelTabExtractor; -import org.schabi.newpipe.extractor.channel.tabs.ChannelTabs; import org.schabi.newpipe.extractor.exceptions.ExtractionException; import org.schabi.newpipe.extractor.exceptions.ParsingException; import org.schabi.newpipe.extractor.playlist.PlaylistInfoItem; @@ -107,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 593096653..62d52cfe4 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 @@ -65,6 +65,7 @@ import free.rm.skytube.app.Settings; 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.POJOs.YouTubeVideo; @@ -208,10 +209,10 @@ 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.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; } @@ -254,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 { @@ -268,8 +269,9 @@ 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); return new VideoPagerWithChannel(streamingService, channelExtractor.findVideosTab(), channelExtractor.channel); } catch (ParsingException | RuntimeException e) { @@ -277,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); @@ -310,23 +312,22 @@ 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(ChannelId channelId, PersistentChannel persistentChannel) throws NewPipeException { Logger.i(this, "Fetching channel details for " + channelId); - VideoPagerWithChannel pager = getChannelPager(Objects.requireNonNull(channelId, "channelId").getRawId()); + VideoPagerWithChannel pager = getChannelPager(channelId); // get the channel, and add all the videos from the first page - YouTubeChannel channel = pager.getChannel(); try { - channel.getYouTubeVideos().addAll(pager.getNextPageAsVideos()); + 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 { @@ -358,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/YouTube/newpipe/VideoPagerWithChannel.java b/app/src/main/java/free/rm/skytube/businessobjects/YouTube/newpipe/VideoPagerWithChannel.java index 3cb96735e..bbad0e387 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,14 +16,20 @@ */ 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; 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 +54,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(@Nullable 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/BookmarksDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/BookmarksDb.java index b3d5c9882..e8e84a706 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; /** @@ -170,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; @@ -227,7 +221,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 +229,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 +238,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 106eafe86..505f3600c 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 63654b426..000000000 --- a/app/src/main/java/free/rm/skytube/businessobjects/db/Column.java +++ /dev/null @@ -1,29 +0,0 @@ -package free.rm.skytube.businessobjects.db; - -import android.database.Cursor; - -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 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 8976c8b48..8e0006f8e 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,26 @@ 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.businessobjects.model.Status; 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 +47,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 +63,37 @@ 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)) { + 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; + } + + 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 +110,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 +164,30 @@ 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(() -> { + PersistentChannel channel = DatabaseTasks.getChannelOrRefresh(context, channelId, true); + SubscriptionsDb db = SubscriptionsDb.getSubscriptionsDb(); + final DatabaseResult result; if (subscribeToChannel) { - return SubscriptionsDb.getSubscriptionsDb().subscribe(channel); + result = db.subscribe(channel, channel.channel().getYouTubeVideos()); } else { - return SubscriptionsDb.getSubscriptionsDb().unsubscribe(channel.getChannelId()); + result = 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 +219,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/DownloadedVideosDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/DownloadedVideosDb.java index fba19361b..8084cd8b1 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 1abbc1034..67e277364 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,9 +17,22 @@ package free.rm.skytube.businessobjects.db; +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 = "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"; @@ -27,9 +40,14 @@ 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"); + 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.name(), LocalChannelTable.TABLE_NAME); - public static final String[] ALL_COLUMNS = new String[]{ - LocalChannelTable.COL_CHANNEL_ID, + private static final String[] ALL_COLUMNS = new String[]{ + LocalChannelTable.COL_CHANNEL_ID.name(), LocalChannelTable.COL_TITLE, LocalChannelTable.COL_DESCRIPTION, LocalChannelTable.COL_BANNER_URL, @@ -38,9 +56,11 @@ 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 + " (" + - COL_CHANNEL_ID + " TEXT UNIQUE NOT NULL, " + + (withPk ? COL_ID.format() + "," : "") + + COL_CHANNEL_ID.format() + ", " + + COL_STATE.format() + ", " + COL_TITLE + " TEXT, " + COL_DESCRIPTION + " TEXT, " + COL_THUMBNAIL_NORMAL_URL+ " TEXT, " + @@ -51,4 +71,27 @@ public static String getCreateStatement() { " )"; } + public static final void addIdColumn(SQLiteDatabase db) { + final String allRowNames = Joiner.on(',').join(ALL_COLUMNS); + 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[]{ + 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); + } + + 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/PlaybackStatusDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/PlaybackStatusDb.java index 909d05272..070383e51 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/SQLiteOpenHelperEx.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SQLiteOpenHelperEx.java index 62ecf29be..7113094bb 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,108 +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); - } - - /** - * 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 void createTable(SQLiteDatabase db, String tableName, Column... columns) { - try { - db.execSQL("CREATE TABLE " + tableName + " (" + listColumns(false, columns) + ")"); - } catch (SQLiteException e) { - Logger.e(db, e,"Unable to create table: '%s', because: %s", tableName, e.getMessage()); - throw e; - } - } - - private static String listColumns(boolean justNames, final Column[] columns) { - boolean first = true; - final StringBuilder sql = new StringBuilder(); - for (Column col : columns) { - 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/SearchHistoryDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SearchHistoryDb.java index c1bcb4480..6edf2ce0f 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/businessobjects/db/SubscriptionsDb.java b/app/src/main/java/free/rm/skytube/businessobjects/db/SubscriptionsDb.java index 770068b87..9302ebd9a 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; @@ -39,15 +41,22 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; +import androidx.annotation.NonNull; +import androidx.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 free.rm.skytube.businessobjects.model.Status; +import io.reactivex.rxjava3.core.Maybe; import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.schedulers.Schedulers; @@ -57,34 +66,30 @@ 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", 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,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 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 volatile SubscriptionsDb subscriptionsDb = null; - private static final int DATABASE_VERSION = 11; + private static final String GET_PK_FROM_CHANNEL_ID = "SELECT " + SubscriptionsTable.COL_ID + " FROM " + SubscriptionsTable.TABLE_NAME + " WHERE " + SubscriptionsTable.COL_CHANNEL_ID + " = ?"; - private static final String DATABASE_NAME = "subs.db"; + private static volatile SubscriptionsDb subscriptionsDb = null; - private Gson gson; + private static final int DATABASE_VERSION = 18; - 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) { @@ -97,8 +102,10 @@ public static synchronized SubscriptionsDb getSubscriptionsDb() { @Override public void onCreate(SQLiteDatabase db) { db.execSQL(SubscriptionsTable.getCreateStatement()); - SubscriptionsVideosTable.addNewFlatTable(db); - db.execSQL(LocalChannelTable.getCreateStatement()); + SubscriptionsVideosTable.addNewFlatTable(db, false); + SubscriptionsVideosTable.addPublishTimeIndex(db); + db.execSQL(LocalChannelTable.getCreateStatement(true)); + LocalChannelTable.addChannelIdIndex(db); db.execSQL(CategoriesTable.getCreateStatement()); new CategoryManagement(db).setupDefaultCategories(); } @@ -111,15 +118,15 @@ 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()); - db.execSQL(LocalChannelTable.getCreateStatement()); + SQLiteHelper.execSQLUpdates(db, SubscriptionsTable.getLastCheckTimeColumn()); + db.execSQL(LocalChannelTable.getCreateStatement(false)); try { for (YouTubeChannel channel : getSubscribedChannels(db)) { if (!Utils.isEmpty(channel.getId()) && @@ -127,7 +134,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) { @@ -138,12 +145,12 @@ 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); } if (upgrade.executeStep(9)) { - SubscriptionsVideosTable.addNewFlatTable(db); + SubscriptionsVideosTable.addNewFlatTable(db, true); migrateFromJsonColumn(db); } if (upgrade.executeStep(10)) { @@ -152,11 +159,68 @@ 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); + } + if (upgrade.executeStep(17)) { + Logger.w(this, "Remove channel title from subscription_videos table"); + 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) { + 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) { Logger.w(this, "Dropping old Subs table"); - continueOnError(db, SubscriptionsVideosTable.getDropTableStatement()); + SQLiteHelper.continueOnError(db, SubscriptionsVideosTable.getDropTableStatement()); } private void fixChannelIds(final SQLiteDatabase db) { @@ -166,8 +230,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 +250,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 +300,44 @@ 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.channelPk(), persistentChannel.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(long channelPk, 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_CHANNEL_PK.name(), channelPk); 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 +348,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()}); - - // remove this channel from the subscriptions DB - int rowsDeleted = getWritableDatabase().delete(SubscriptionsTable.TABLE_NAME, - SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channelId.getRawId()}); + 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() + " = ?", + toArray(channel.subscriptionPk())); + + // 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); @@ -313,7 +382,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)); @@ -328,7 +397,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)); @@ -346,39 +415,44 @@ 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()); - 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()); - values.put(SubscriptionsVideosTable.COL_PUBLISH_TIME.name, video.getPublishTimestamp()); + if (persistentChannel != null) { + 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()); + } + + 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, @@ -389,7 +463,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))); @@ -403,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 * * @@ -413,24 +499,20 @@ 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 colCategoryId = cursor.getColumnIndexOrThrow(SubscriptionsTable.COL_CATEGORY_ID.name); + 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); + 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 { final String id = cursor.getString(colChannelIdNum); @@ -450,44 +532,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. * @@ -498,10 +542,9 @@ public YouTubeChannel getCachedSubscribedChannel(ChannelId channelId) { public boolean isUserSubscribedToChannel(ChannelId channelId) { SkyTubeApp.nonUiThread(); - return executeQueryForInteger(IS_SUBSCRIBED_QUERY, new String[]{channelId.getRawId()}, 0) > 0; + return SQLiteHelper.executeQueryForInteger(getReadableDatabase(), IS_SUBSCRIBED_QUERY, toArrayParam(channelId), 0) > 0; } - public Single getUserSubscribedToChannel(ChannelId channelId) { return Single.fromCallable(() -> isUserSubscribedToChannel(channelId)).subscribeOn(Schedulers.io()); } @@ -513,7 +556,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(); @@ -525,129 +568,53 @@ 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()); } - /** - * 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 updateLastCheckTime(String channelId) { - SQLiteDatabase db = getWritableDatabase(); - long currentTime = System.currentTimeMillis(); - - ContentValues values = new ContentValues(); - values.put(SubscriptionsTable.COL_LAST_CHECK_TIME, currentTime); - - int count = db.update( - SubscriptionsTable.TABLE_NAME, - values, - SubscriptionsTable.COL_CHANNEL_ID + " = ?", - new String[]{channelId}); - - 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); - } - } + return SQLiteHelper.executeQueryForInteger(getReadableDatabase(), HAS_VIDEO_QUERY, new String[]{video.getId()}, 0) > 0; } /** * 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); - } - } - } - } - - /** - * Insert videos into the subscription video table. - * @param videos - */ - public void insertVideosForChannel(List videos, ChannelId channelId) { + public void saveChannelVideos(Collection videos, PersistentChannel persistentChannel, boolean doUpdate) { SkyTubeApp.nonUiThread(); - SQLiteDatabase db = getWritableDatabase(); + long latestPublishTimestamp = 0; + for (YouTubeVideo video : videos) { - try { - if (video.getPublishTimestamp() != null) { - ContentValues values = createContentValues(video, channelId); + 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); } - } catch (Exception e) { - Logger.e(this, e, "Error inserting "+ videos + " - "+e.getMessage()); } } + SubscriptionsTable.updateLastVideoFetchTimestamps(db, persistentChannel); + LocalChannelTable.updateLatestVideoTimestamp(db, persistentChannel, latestPublishTimestamp); } - 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); - 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; } @@ -660,36 +627,23 @@ 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 = "(" + 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); - } - - 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(); - } + 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); + } + } /** * Load YouTubeVideo objects from a cursor, only SubscriptionsVideosTable.COL_YOUTUBE_VIDEO column is needed. @@ -705,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); @@ -747,132 +701,147 @@ 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)) { - if (cursor.moveToNext()) { + 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 = ?", + toArrayParam(channelId))) { + if (cursor.moveToNext()) { + 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 = 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 = 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, statusCode); + } } 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); - } + /** + * 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 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; - } + private Status getStatusCode(Cursor cursor) { + return Status.lookup(SQLiteHelper.getLong(cursor, LocalChannelTable.COL_STATE.name())); + } - 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()); - 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()); - } + private String[] toArray(Object obj) { + return new String[] { String.valueOf(obj)}; + } - 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; - } + private String[] toArrayParam(ChannelId channelId) { + return new String[] { channelId.getRawId() }; + } - /** - * 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); - } + 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); + } + } + return null; + } + + private PersistentChannel cacheChannel(SQLiteDatabase db, @Nullable PersistentChannel persistentChannel, YouTubeChannel channel) { + 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; + + // 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, 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, status); + } + + private static ContentValues toContentValues(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()); + } + 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)) { + 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(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"); + 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 bb7d9d4fa..fcbb25ed0 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,86 @@ 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; + /** * 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"; + public static final Column COL_CHANNEL_PK = new Column("channel_pk", "integer"); + + private static final String ADD_COLUMN = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN "; - private static final String ADD_COLUMN = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN "; + 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 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 " + - " )"; - } + 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 " + + " )"; + } - 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[] 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); + SQLiteHelper.addColumn(db, TABLE_NAME, COL_CATEGORY_ID); + } + + public static String[] getLastCheckTimeColumn() { + return new String[]{ADD_COLUMN + COL_LAST_CHECK_TIME + " INTEGER "}; + } + + public static void cleanupTable(SQLiteDatabase db) { + 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 String[] getLastCheckTimeColumn() { - return new String[] { ADD_COLUMN + COL_LAST_CHECK_TIME + " INTEGER "}; - } + /** + * 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[] { + 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 + ")"); + } } 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 8470703c3..047d141d0 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,12 @@ import android.database.sqlite.SQLiteDatabase; +import com.github.skytube.components.utils.Column; +import com.github.skytube.components.utils.SQLiteHelper; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + /** * A table that caches metadata about videos published by subbed channels. */ @@ -49,28 +55,41 @@ 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 + " = ?"; 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, - 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; + 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() { @@ -106,13 +125,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, @@ -122,7 +142,34 @@ public static void addNewFlatTable(SQLiteDatabase db) { COL_DURATION, COL_PUBLISH_TIME, COL_PUBLISH_TIME_EXACT - ); - SQLiteOpenHelperEx.createIndex(db, IDX_PUBLISH_TS_V2, TABLE_NAME_V2, COL_CATEGORY_ID); + }; + } + + public static void addNewFlatTable(SQLiteDatabase db, boolean withChannelTitle) { + 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) { + SQLiteHelper.addColumn(db, TABLE_NAME_V2, COL_SUBS_ID); + } + + static void addChannelPkColumn(SQLiteDatabase db) { + SQLiteHelper.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(",")); + + 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) { + SQLiteHelper.createIndex(db, IDX_PUBLISH_TIMESTAMP, TABLE_NAME_V2, COL_PUBLISH_TIME); } } 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 000000000..d88da27ae --- /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 cc67f45ad..cb3bc48ac 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,9 +51,10 @@ 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.YouTubeChannel; +import free.rm.skytube.businessobjects.YouTube.POJOs.PersistentChannel; 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.DatabaseTasks; import free.rm.skytube.businessobjects.db.SubscriptionsDb; @@ -493,11 +495,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/SearchHistoryCursorAdapter.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/SearchHistoryCursorAdapter.java index 4c30971ee..8ee24000e 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); } 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 089ad3db3..8b48adbc2 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; /** @@ -142,7 +139,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)); } @@ -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/java/free/rm/skytube/gui/businessobjects/adapters/VideoGridAdapter.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/adapters/VideoGridAdapter.java index f15e0b4be..24487cdc8 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/businessobjects/views/ChannelActionHandler.java b/app/src/main/java/free/rm/skytube/gui/businessobjects/views/ChannelActionHandler.java index a53552702..14539f283 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,11 +41,11 @@ 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, - 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 c335ad741..cb0528951 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,12 +59,8 @@ 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, 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 f764d22dc..366f6ce17 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/SubscriptionsFeedFragment.java b/app/src/main/java/free/rm/skytube/gui/fragments/SubscriptionsFeedFragment.java index cfa0273df..c1786e94b 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); 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 5a7b6f75c..1189cdd7e 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 10e2743ad..85e3cfb60 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) { diff --git a/app/src/main/res/values/strings_subs.xml b/app/src/main/res/values/strings_subs.xml index 9f17b5d23..6959ffd0c 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] +