11<template >
22 <h1 v-t =" 'titles.trending'" class =" my-4 text-center font-bold" />
3-
43 <hr />
5-
64 <LoadingIndicatorPage
75 :show-content =" videos .length != 0 "
86 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 "
@@ -18,29 +16,108 @@ import { useI18n } from "vue-i18n";
1816import LoadingIndicatorPage from " ./LoadingIndicatorPage.vue" ;
1917import VideoItem from " ./VideoItem.vue" ;
2018import { fetchJson , apiUrl } from " @/composables/useApi.js" ;
21- import { getPreferenceString } from " @/composables/usePreferences.js" ;
19+ import { getPreferenceString , getPreferenceBoolean } from " @/composables/usePreferences.js" ;
2220import { updateWatched } from " @/composables/useMisc.js" ;
2321import { fetchDeArrowContent } from " @/composables/useSubscriptions.js" ;
2422import { getHomePage } from " @/composables/useMisc.js" ;
2523
2624const route = useRoute ();
2725const router = useRouter ();
2826const { t } = useI18n ();
29-
3027const videos = ref ([]);
3128
32- async function fetchTrending (region ) {
33- return await fetchJson (apiUrl () + " /trending" , {
34- region: region || " US" ,
29+ function idbCursorToPromise (store , fn ) {
30+ return new Promise ((resolve , reject ) => {
31+ const req = store .openCursor ();
32+ req .onerror = () => reject (req .error );
33+ req .onsuccess = e => {
34+ const cursor = e .target .result ;
35+ if (cursor) {
36+ fn (cursor .value );
37+ cursor .continue ();
38+ } else {
39+ resolve ();
40+ }
41+ };
42+ });
43+ }
44+
45+ async function getPreferredChannels () {
46+ if (! window .db || ! getPreferenceBoolean (" watchHistory" , false )) return [];
47+
48+ const tx = window .db .transaction (" watch_history" , " readonly" );
49+ const store = tx .objectStore (" watch_history" );
50+ const counts = new Map ();
51+
52+ await idbCursorToPromise (store, video => {
53+ if (video .uploaderUrl ) {
54+ const id = video .uploaderUrl .split (" /" ).pop ();
55+ counts .set (id, (counts .get (id) || 0 ) + 1 );
56+ }
3557 });
58+
59+ return Array .from (counts .entries ())
60+ .sort ((a , b ) => b[1 ] - a[1 ])
61+ .slice (0 , 10 )
62+ .map (([id ]) => id);
63+ }
64+
65+ async function fetchChannelVideos (channelIds ) {
66+ const results = await Promise .allSettled (channelIds .map (id => fetchJson (apiUrl () + " /channel/" + id)));
67+ return results
68+ .filter (r => r .status === " fulfilled" && r .value ? .relatedStreams )
69+ .flatMap (r => r .value .relatedStreams .slice (0 , 5 ));
70+ }
71+
72+ function interleave (trending , recommended ) {
73+ const out = [];
74+ let ti = 0 ,
75+ ri = 0 ;
76+ while (ti < trending .length || ri < recommended .length ) {
77+ if (ti < trending .length ) out .push (trending[ti++ ]);
78+ if (ti < trending .length ) out .push (trending[ti++ ]);
79+ if (ti < trending .length ) out .push (trending[ti++ ]);
80+ if (ri < recommended .length ) out .push (recommended[ri++ ]);
81+ }
82+ return out;
83+ }
84+
85+ async function fetchTrending (region ) {
86+ const personalizedTrending = getPreferenceBoolean (" personalizedTrending" , false );
87+ const personalizedTrendingOnly = getPreferenceBoolean (" personalizedTrendingOnly" , false );
88+
89+ if (! personalizedTrending) {
90+ return await fetchJson (apiUrl () + " /trending" , { region: region || " US" });
91+ }
92+
93+ const [trending , preferredChannels ] = await Promise .all ([
94+ personalizedTrendingOnly ? Promise .resolve ([]) : fetchJson (apiUrl () + " /trending" , { region: region || " US" }),
95+ getPreferredChannels (),
96+ ]);
97+
98+ if (preferredChannels .length === 0 ) return trending;
99+
100+ const recommended = await fetchChannelVideos (preferredChannels);
101+
102+ const seen = new Set ();
103+ const dedup = arr =>
104+ arr .filter (v => {
105+ const id = v .url ? .split (" v=" )[1 ];
106+ if (! id || seen .has (id)) return false ;
107+ seen .add (id);
108+ return true ;
109+ });
110+
111+ if (personalizedTrendingOnly) return dedup (recommended);
112+
113+ return interleave (dedup (trending), dedup (recommended));
36114}
37115
38116onMounted (() => {
39117 if (route .path == import .meta.env.BASE_URL && getPreferenceString(" homepage" , " trending" ) == " feed" ) {
40118 return ;
41119 }
42- let region = getPreferenceString (" region" , " US" );
43-
120+ const region = getPreferenceString (" region" , " US" );
44121 fetchTrending (region).then (vids => {
45122 videos .value = vids;
46123 updateWatched (videos .value );
@@ -52,7 +129,7 @@ onActivated(() => {
52129 document .title = t (" titles.trending" ) + " - Piped" ;
53130 if (videos .value .length > 0 ) updateWatched (videos .value );
54131 if (route .path == import .meta.env.BASE_URL) {
55- let homepage = getHomePage ();
132+ const homepage = getHomePage ();
56133 if (homepage !== undefined ) router .push (homepage );
57134 }
58135});
0 commit comments