Skip to content
7 changes: 5 additions & 2 deletions src/components/FeedPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
to="/subscriptions"
/>
<a
v-if="getRssUrl"
:href="getRssUrl"
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
:aria-label="$t('actions.rss_feed')"
Expand Down Expand Up @@ -94,7 +95,9 @@ const channelGroups = ref([]);

const getRssUrl = computed(() => {
if (isAuthenticated()) return authApiUrl() + "/feed/rss?authToken=" + getAuthToken();
else return authApiUrl() + "/feed/unauthenticated/rss?channels=" + getUnauthenticatedChannels();
const channels = getUnauthenticatedChannels();
if (!channels) return null;
return authApiUrl() + "/feed/unauthenticated/rss?channels=" + channels;
});

const filteredVideos = computed(() => {
Expand All @@ -112,7 +115,7 @@ const filteredVideos = computed(() => {
function loadMoreVideos() {
if (!videosStore.value) return;
currentVideoCount = Math.min(currentVideoCount + videoStep, videosStore.value.length);
if (videos.value.length != videosStore.value.length) {
if (videos.value.length !== videosStore.value.length) {
fetchDeArrowContent(videosStore.value.slice(videos.value.length, currentVideoCount));
videos.value = videosStore.value.slice(0, currentVideoCount);
}
Expand Down
15 changes: 14 additions & 1 deletion src/components/PreferencesPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,6 @@
for="chkMinimizeChapters"
>
<strong v-t="'actions.chapters_layout_mobile'" />

<select
id="ddlDefaultHomepage"
v-model="mobileChapterLayout"
Expand Down Expand Up @@ -199,6 +198,18 @@
</template>
<PreferenceSwitch id="chkHideWatched" v-model="hideWatched" @change="onChange" />
</PreferenceRow>
<PreferenceRow v-if="watchHistory" for-id="chkPersonalizedTrending">
<template #label>
<strong v-t="'actions.personalized_trending'" />
</template>
<PreferenceSwitch id="chkPersonalizedTrending" v-model="personalizedTrending" @change="onChange" />
</PreferenceRow>
<PreferenceRow v-if="watchHistory && personalizedTrending" for-id="chkPersonalizedTrendingOnly">
<template #label>
<strong v-t="'actions.personalized_trending_only'" />
</template>
<PreferenceSwitch id="chkPersonalizedTrendingOnly" v-model="personalizedTrendingOnly" @change="onChange" />
</PreferenceRow>
<PreferenceRow for-id="ddlEnabledCodecs">
<template #label>
<strong v-t="'actions.enabled_codecs'" />
Expand Down Expand Up @@ -529,6 +540,8 @@ const searchSuggestions = usePreferenceBoolean("searchSuggestions", true);
const watchHistory = usePreferenceBoolean("watchHistory", false);
const searchHistory = usePreferenceBoolean("searchHistory", false);
const hideWatched = usePreferenceBoolean("hideWatched", false);
const personalizedTrending = usePreferenceBoolean("personalizedTrending", false);
const personalizedTrendingOnly = usePreferenceBoolean("personalizedTrendingOnly", false);
const selectedLanguage = usePreferenceString("hl", "en");
const languages = [
{ code: "ar", name: "Arabic" },
Expand Down
155 changes: 144 additions & 11 deletions src/components/TrendingPage.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
<template>
<h1 v-t="'titles.trending'" class="my-4 text-center font-bold" />

<hr />

<LoadingIndicatorPage
:show-content="videos.length != 0"
:show-content="loaded"
class="mx-2 grid grid-cols-1 gap-y-5 max-md:gap-x-3 sm:mx-0 sm:grid-cols-2 md:grid-cols-3 md:gap-x-6 lg:grid-cols-4 xl:grid-cols-5"
>
<p
v-if="loaded && videos.length === 0 && personalizedOnly"
v-t="'info.no_watch_history_trending'"
class="col-span-full text-center text-gray-500"
/>
Comment thread
coderabbitai[bot] marked this conversation as resolved.
<VideoItem v-for="video in videos" :key="video.url" :item="video" height="118" width="210" />
</LoadingIndicatorPage>
</template>
Expand All @@ -18,42 +21,172 @@ import { useI18n } from "vue-i18n";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import VideoItem from "./VideoItem.vue";
import { fetchJson, apiUrl } from "@/composables/useApi.js";
import { getPreferenceString } from "@/composables/usePreferences.js";
import { getPreferenceString, getPreferenceBoolean } from "@/composables/usePreferences.js";
import { updateWatched } from "@/composables/useMisc.js";
import { fetchDeArrowContent } from "@/composables/useSubscriptions.js";
import { getHomePage } from "@/composables/useMisc.js";

const route = useRoute();
const router = useRouter();
const { t } = useI18n();

const videos = ref([]);
const loaded = ref(false);
const personalizedOnly = ref(false);
const firstActivation = ref(true);

async function fetchTrending(region) {
return await fetchJson(apiUrl() + "/trending", {
region: region || "US",
function idbCursorToPromise(store, fn) {
return new Promise((resolve, reject) => {
const req = store.openCursor();
req.onerror = () => reject(req.error);
req.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
fn(cursor.value);
cursor.continue();
} else {
resolve();
}
};
});
}

async function getPreferredChannels() {
if (!window.db || !getPreferenceBoolean("watchHistory", false)) return { ids: [], count: 0 };

const tx = window.db.transaction("watch_history", "readonly");
const store = tx.objectStore("watch_history");
const counts = new Map();

const countReq = store.count();
const historyCount = await new Promise((res, rej) => {
countReq.onsuccess = () => res(countReq.result);
countReq.onerror = () => rej(countReq.error);
});

await idbCursorToPromise(store, video => {
if (video.uploaderUrl) {
const id = video.uploaderUrl.replace(/\/+$/, "").split("/").pop();
if (id) counts.set(id, (counts.get(id) || 0) + 1);
}
});

return {
ids: Array.from(counts.entries())
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.map(([id]) => id),
count: historyCount,
};
}
Comment thread
quantumvoid0 marked this conversation as resolved.

const channelCache = new Map();
const CHANNEL_CACHE_TTL = 5 * 60 * 1000;

async function fetchChannelVideos(channelIds, historyCount) {
const cacheKey = channelIds.slice().sort().join(",") + "@" + historyCount;
const cached = channelCache.get(cacheKey);
if (cached && Date.now() - cached.ts < CHANNEL_CACHE_TTL) return cached.data;

const feeds = await fetchJson(apiUrl() + "/feeds/unauthenticated?channels=" + channelIds.join(","));
const data = (feeds ?? []).slice().sort((a, b) => (b.uploaded ?? 0) - (a.uploaded ?? 0));
channelCache.set(cacheKey, { ts: Date.now(), data });
return data;
}

function interleave(trending, recommended) {
const out = [];
let ti = 0,
ri = 0;
while (ti < trending.length || ri < recommended.length) {
for (let i = 0; i < 3 && ti < trending.length; i++) out.push(trending[ti++]);
if (ri < recommended.length) out.push(recommended[ri++]);
}
return out;
}

async function fetchTrending(region) {
const personalizedTrending = getPreferenceBoolean("personalizedTrending", false);
const personalizedTrendingOnly = getPreferenceBoolean("personalizedTrendingOnly", false);

if (!personalizedTrending) {
return await fetchJson(apiUrl() + "/trending", { region: region || "US" });
}

const plainTrending = personalizedTrendingOnly
? Promise.resolve([])
: fetchJson(apiUrl() + "/trending", { region: region || "US" });

try {
const [trending, { ids: preferredChannels, count: historyCount }] = await Promise.all([
plainTrending,
getPreferredChannels(),
]);

if (preferredChannels.length === 0) return trending;

const recommended = await fetchChannelVideos(preferredChannels, historyCount);

const seen = new Set();
const dedup = arr =>
arr.filter(v => {
const qs = v.url?.includes("?") ? v.url.slice(v.url.indexOf("?")) : "";
const id = qs ? new URLSearchParams(qs).get("v") : null;
if (!id || seen.has(id)) return false;
seen.add(id);
return true;
});

if (personalizedTrendingOnly) return dedup(recommended);

return interleave(dedup(trending), dedup(recommended));
} catch {
return await fetchJson(apiUrl() + "/trending", { region: region || "US" });
}
}

function updatePersonalizedOnly() {
personalizedOnly.value =
getPreferenceBoolean("personalizedTrending", false) && getPreferenceBoolean("personalizedTrendingOnly", false);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

onMounted(() => {
if (route.path == import.meta.env.BASE_URL && getPreferenceString("homepage", "trending") == "feed") {
firstActivation.value = false;
return;
}
let region = getPreferenceString("region", "US");

updatePersonalizedOnly();
const region = getPreferenceString("region", "US");
fetchTrending(region).then(vids => {
videos.value = vids;
loaded.value = true;
updateWatched(videos.value);
fetchDeArrowContent(videos.value);
firstActivation.value = false;
});
});

onActivated(() => {
document.title = t("titles.trending") + " - Piped";
if (videos.value.length > 0) updateWatched(videos.value);
if (route.path == import.meta.env.BASE_URL) {
let homepage = getHomePage();
const homepage = getHomePage();
if (homepage !== undefined) router.push(homepage);
}
if (firstActivation.value) {
firstActivation.value = false;
return;
}
if (getPreferenceBoolean("personalizedTrending", false)) {
updatePersonalizedOnly();
channelCache.clear();
loaded.value = false;
const region = getPreferenceString("region", "US");
fetchTrending(region).then(vids => {
videos.value = vids;
loaded.value = true;
updateWatched(videos.value);
fetchDeArrowContent(videos.value);
});
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
</script>
5 changes: 4 additions & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@
"time_code": "Time code (in seconds or HH:MM:SS)",
"show_chapters": "Chapters",
"store_search_history": "Store Search History",
"hide_watched": "Hide watched videos in the feed",
"hide_watched": "Hide watched videos in the feed",
"personalized_trending": "Personalize trending page",
"personalized_trending_only": "Show only recommendations (hide global trending)",
"mark_as_watched": "Mark as Watched",
"mark_as_unwatched": "Mark as Unwatched",
"documentation": "Documentation",
Expand Down Expand Up @@ -234,6 +236,7 @@
"subscribed_channels_count": "Subscribed to: {0}"
},
"info": {
"no_watch_history_trending": "No history found to personalize trending page",
"preferences_note": "Note: preferences are saved in the local storage of your browser. Deleting your browser data will reset them.",
"page_not_found": "Page not found",
"copied": "Copied!",
Expand Down