Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
162 changes: 151 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,179 @@ 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 results = await Promise.allSettled(channelIds.map(id => fetchJson(apiUrl() + "/channel/" + id)));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you use the unauthenticated channel feeds api, since this is quite expensive on the server? You can pass multiple channel IDs to it too.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

surely, ill implement the fix..but there seems to be another issue preventing the code in this PR from running, about a week ago i noticed that videos section in channels no longer load, and this affected the trending page too as it reads from there, i dont think its just my side, bcz the piped.video server also had the same issue.

const data = results
.filter(r => r.status === "fulfilled" && r.value?.relatedStreams)
.flatMap(r =>
r.value.relatedStreams
.slice()
.sort((a, b) => (b.uploaded ?? 0) - (a.uploaded ?? 0))
.slice(0, 5),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
);
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
Loading