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
6 changes: 5 additions & 1 deletion src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ onMounted(() => {
darkModePreference.addEventListener("change", handlePreferredColorSchemeChange);

if ("indexedDB" in window) {
const request = indexedDB.open("piped-db", 6);
const request = indexedDB.open("piped-db", 7);
request.onupgradeneeded = ev => {
const db = request.result;
console.log("Upgrading object store.");
Expand Down Expand Up @@ -118,6 +118,10 @@ onMounted(() => {
const playlistVideosStore = db.createObjectStore("playlist_videos", { keyPath: "videoId" });
playlistVideosStore.createIndex("videoId", "videoId", { unique: true });
}
if (!db.objectStoreNames.contains("blocked_channels")) {
const store = db.createObjectStore("blocked_channels", { keyPath: "channelId" });
store.createIndex("channelId", "channelId", { unique: true });
}
// migration to fix an invalid previous length of channel ids: 11 -> 24
(async () => {
if (ev.oldVersion < 6) {
Expand Down
18 changes: 18 additions & 0 deletions src/components/ChannelPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
</div>

<div class="flex gap-2">
<button
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
@click="blockHandler"
v-text="$t('actions.' + (blocked ? 'unblock' : 'block'))"
></button>

<button
class="inline-block w-auto cursor-pointer rounded-sm bg-gray-300 py-2 text-gray-600 hover:bg-gray-500 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 max-md:px-2 md:px-4 dark:bg-dark-400 dark:text-gray-400 dark:hover:bg-dark-300"
@click="subscribeHandler"
Expand Down Expand Up @@ -105,6 +111,7 @@ import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import CollapsableText from "./CollapsableText.vue";
import AddToGroupModal from "./AddToGroupModal.vue";
import { fetchJson, apiUrl } from "@/composables/useApi.js";
import { isChannelBlocked, toggleChannelBlock } from "@/composables/useChannels.js";
import { numberFormat } from "@/composables/useFormatting.js";
import {
fetchSubscriptionStatus,
Expand All @@ -118,6 +125,7 @@ const { t } = useI18n();

const channel = ref(null);
const subscribed = ref(false);
const blocked = ref(false);
const tabs = ref([]);
const selectedTab = ref(0);
const contentItems = ref([]);
Expand All @@ -129,6 +137,11 @@ async function fetchSubscribedStatus() {
subscribed.value = await fetchSubscriptionStatus(channel.value.id);
}

async function fetchBlockedStatus() {
if (!channel.value.id) return;
blocked.value = await isChannelBlocked(channel.value.id);
}
Comment on lines +140 to +143
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

blocked state becomes stale when component is re-activated from cache

fetchBlockedStatus() is only called from getChannelData() (triggered in onMounted). When the component re-activates from keep-alive (e.g., after the user unblocks a channel in Preferences and navigates back), blocked.value retains its stale value and the button shows the wrong label.

fetchSubscribedStatus has the same gap, but this is a new feature and trivial to fix:

🛠️ Proposed fix
 onActivated(() => {
     if (channel.value && !channel.value.error) document.title = channel.value.name + " - Piped";
     window.addEventListener("scroll", handleScroll);
     if (channel.value && !channel.value.error) updateWatched(channel.value.relatedStreams);
+    if (channel.value && !channel.value.error) fetchBlockedStatus();
 });

Also applies to: 282-286

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/components/ChannelPage.vue` around lines 140 - 143, fetchBlockedStatus
(and similarly fetchSubscribedStatus) is only invoked from
getChannelData/onMounted so when the component is re-activated from keep-alive
the blocked/subscribed refs can become stale; update the component to also
refresh these statuses in the Vue onActivated lifecycle hook (or call
fetchBlockedStatus and fetchSubscribedStatus from a shared refresh function
invoked by both onMounted and onActivated), ensuring you keep the existing guard
(if (!channel.value.id) return) and apply the same fix for the other occurrence
referenced around the 282-286 area so the button labels reflect current state
after navigation.


async function fetchChannel() {
const url = route.path.includes("@")
? apiUrl() + "/@/" + route.params.channelId
Expand All @@ -144,6 +157,7 @@ async function getChannelData() {
document.title = channel.value.name + " - Piped";
contentItems.value = channel.value.relatedStreams;
fetchSubscribedStatus();
fetchBlockedStatus();
updateWatched(channel.value.relatedStreams);
fetchDeArrowContent(channel.value.relatedStreams);
tabs.value.push({
Expand Down Expand Up @@ -209,6 +223,10 @@ function subscribeHandler() {
});
}

async function blockHandler() {
blocked.value = await toggleChannelBlock(channel.value.id, channel.value.name);
}

function getTranslatedTabName(tabName) {
let translatedTabName = tabName;
switch (tabName) {
Expand Down
36 changes: 36 additions & 0 deletions src/components/PreferencesPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,30 @@
</div>
<br />
</div>
<h2 v-t="'titles.blocked_channels'" class="text-center"></h2>
<table class="w-full border text-left text-lg font-light">
<thead>
<tr>
<th v-t="'preferences.channel'" />
<th />
</tr>
</thead>
<tbody>
<tr v-for="channel in blockedChannels" :key="channel.channelId">
<td>
<a :href="`/channel/${channel.channelId}`" v-text="channel.name" />
</td>
<td class="text-end">
<button
class="inline-flex items-center gap-1 rounded-sm px-3 py-2 text-gray-700 hover:text-white focus:shadow-red-400 focus:outline-2 focus:outline-red-500 dark:text-gray-300"
@click="unblockChannel(channel.channelId)"
>
<i-fa6-solid-trash class="shrink-0" />
</button>
</td>
</tr>
</tbody>
</table>
<h2 id="instancesList" v-t="'actions.instances_list'" />
<table class="w-full border text-left text-lg font-light">
<thead>
Expand Down Expand Up @@ -472,6 +496,7 @@ import {
usePreferenceString,
} from "@/composables/usePreferences";
import { fetchJson, apiUrl, authApiUrl, getAuthToken, hashCode, isAuthenticated } from "@/composables/useApi";
import { getBlockedChannels, removeBlockedChannel } from "@/composables/useChannels.js";
import { getCustomInstances } from "@/composables/useCustomInstances";
import { download } from "@/composables/useMisc";
import { getDefaultLanguage } from "@/composables/useFormatting";
Expand Down Expand Up @@ -502,6 +527,7 @@ const authInstance = usePreferenceBoolean("authInstance", false);
const selectedAuthInstance = usePreferenceString("auth_instance_url", selectedInstance.value);
const customInstances = ref([]);
const publicInstances = ref([]);
const blockedChannels = ref([]);
const sponsorBlock = usePreferenceBoolean("sponsorblock", true);
const skipOptionsStorage = usePreferenceJSON("skipOptions", null);
const skipOptions = ref(createDefaultSkipOptions());
Expand Down Expand Up @@ -667,6 +693,7 @@ onMounted(async () => {
if (Object.keys(route.query).length > 0) router.replace({ query: {} });

fetchInstances();
loadBlockedChannels();

if (testLocalStorage()) {
skipOptions.value = normalizeSkipOptions(skipOptionsStorage.value);
Expand Down Expand Up @@ -715,6 +742,15 @@ async function fetchInstances() {
});
}

async function loadBlockedChannels() {
blockedChannels.value = await getBlockedChannels();
}

async function unblockChannel(channelId) {
removeBlockedChannel(channelId);
blockedChannels.value = blockedChannels.value.filter(channel => channel.channelId !== channelId);
}

function sslScore(url) {
return "https://www.ssllabs.com/ssltest/analyze.html?d=" + new URL(url).host + "&latest";
}
Expand Down
9 changes: 9 additions & 0 deletions src/components/VideoItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ import PlaylistAddModal from "./PlaylistAddModal.vue";
import ShareModal from "./ShareModal.vue";
import ConfirmModal from "./ConfirmModal.vue";
import VideoThumbnail from "./VideoThumbnail.vue";
import { isChannelBlocked } from "@/composables/useChannels.js";
import { numberFormat, timeAgo } from "@/composables/useFormatting.js";
import { getPreferenceBoolean } from "@/composables/usePreferences.js";
import { removeVideoFromPlaylist } from "@/composables/usePlaylists.js";
Expand Down Expand Up @@ -180,6 +181,14 @@ function removeVideo() {
}

function shouldShowVideo() {
if (window.location.pathname !== props.item.uploaderUrl) {
isChannelBlocked(props.item.uploaderUrl).then(blocked => {
if (blocked) {
showVideo.value = false;
}
});
}

if (!props.isFeed || !getPreferenceBoolean("hideWatched", false)) return;

const objectStore = window.db.transaction("watch_history", "readonly").objectStore("watch_history");
Expand Down
61 changes: 61 additions & 0 deletions src/composables/useChannels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
function normalizeId(channelId) {
return channelId.replace("/channel/", "");
}

export function isChannelBlocked(channelUrl) {
return new Promise(resolve => {
var tx = window.db.transaction("blocked_channels", "readonly");
var store = tx.objectStore("blocked_channels");
store.count(normalizeId(channelUrl)).onsuccess = e => {
const result = e.target.result;
resolve(result > 0);
};
});
}

export function addBlockedChannel(channelId, name) {
var tx = window.db.transaction("blocked_channels", "readwrite");
var store = tx.objectStore("blocked_channels");
store.add({ channelId, name });
}

export function removeBlockedChannel(channelId) {
var tx = window.db.transaction("blocked_channels", "readwrite");
var store = tx.objectStore("blocked_channels");
store.delete(channelId);
}

export function getBlockedChannels() {
return new Promise(resolve => {
let blockedChannels = [];
var tx = window.db.transaction("blocked_channels", "readonly");
var store = tx.objectStore("blocked_channels");
const cursor = store.index("channelId").openCursor();
cursor.onsuccess = e => {
const cursor = e.target.result;
if (cursor) {
blockedChannels.push(cursor.value);
cursor.continue();
} else {
resolve(blockedChannels);
}
};
});
}

export async function toggleChannelBlock(channelUrl, name) {
const channelId = normalizeId(channelUrl);
return new Promise(resolve => {
var tx = window.db.transaction("blocked_channels", "readwrite");
var store = tx.objectStore("blocked_channels");
store.count(channelId).onsuccess = e => {
const result = e.target.result;
if (result > 0) {
store.delete(channelId);
} else {
store.add({ channelId, name });
}
resolve(result === 0);
};
});
}
Comment on lines +1 to +61
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Inconsistent ID normalization causes lookup/storage mismatches.

normalizeId is applied inconsistently:

  • isChannelBlocked: normalizes before lookup ✓
  • addBlockedChannel: stores raw ID (no normalize) ✗
  • removeBlockedChannel: deletes raw ID (no normalize) ✗
  • toggleChannelBlock: counts with normalized ID, but stores/deletes with raw ID ✗

This breaks when:

  1. Block via uploaderUrl = "/channel/UCxxx" → stores "/channel/UCxxx"
  2. Check via channel.id = "UCxxx" → looks for "UCxxx" → not found (false negative)

Or conversely, duplicate entries could be created for the same channel.

Proposed fix: normalize consistently everywhere
 function normalizeId(channelId) {
     return channelId.replace("/channel/", "");
 }

 export function addBlockedChannel(channelId, name) {
     var tx = window.db.transaction("blocked_channels", "readwrite");
     var store = tx.objectStore("blocked_channels");
-    store.add({ channelId, name });
+    store.add({ channelId: normalizeId(channelId), name });
 }

 export function removeBlockedChannel(channelId) {
     var tx = window.db.transaction("blocked_channels", "readwrite");
     var store = tx.objectStore("blocked_channels");
-    store.delete(channelId);
+    store.delete(normalizeId(channelId));
 }

 export async function toggleChannelBlock(channelId, name) {
     return new Promise(resolve => {
         var tx = window.db.transaction("blocked_channels", "readwrite");
         var store = tx.objectStore("blocked_channels");
-        store.count(normalizeId(channelId)).onsuccess = e => {
+        const normalized = normalizeId(channelId);
+        store.count(normalized).onsuccess = e => {
             const result = e.target.result;
             if (result > 0) {
-                store.delete(channelId);
+                store.delete(normalized);
             } else {
-                store.add({ channelId, name });
+                store.add({ channelId: normalized, name });
             }
             resolve(result === 0);
         };
     });
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/composables/useChannels.js` around lines 1 - 60, The code inconsistently
applies normalizeId causing lookup/storage mismatches; update addBlockedChannel,
removeBlockedChannel and toggleChannelBlock to call normalizeId(channelId)
before storing or deleting (and ensure the stored object uses the normalized id
for its channelId property), keep isChannelBlocked as-is (it already
normalizes), and ensure delete/count operations in toggleChannelBlock use the
same normalized id so lookups and inserts reference the same key.

11 changes: 8 additions & 3 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"bookmarks": "Bookmarks",
"channel_groups": "Channel groups",
"dearrow": "DeArrow",
"custom_instances": "Custom instances"
"custom_instances": "Custom instances",
"blocked_channels": "Blocked Channels"
},
"player": {
"watch_on": "View on {0}",
Expand Down Expand Up @@ -177,7 +178,10 @@
"show_password": "Show password",
"hide_password": "Hide password",
"rss_feed": "RSS feed",
"playlist_rss_feed": "Playlist RSS feed"
"playlist_rss_feed": "Playlist RSS feed",
"blocked_channels": "Blocked channels",
"block": "Block",
"unblock": "Unblock"
},
"comment": {
"pinned_by": "Pinned by {author}",
Expand All @@ -194,7 +198,8 @@
"up_to_date": "Up to date?",
"ssl_score": "SSL Score",
"uptime_30d": "Uptime (30d)",
"api_url": "API URL"
"api_url": "API URL",
"channel": "Channel"
},
"login": {
"username": "Username",
Expand Down
Loading