Explorar o código

feat: expose `fetchVideoVotes` function on interface

Sven hai 10 meses
pai
achega
7f95e9723e
Modificáronse 5 ficheiros con 92 adicións e 43 borrados
  1. 1 0
      changelog.md
  2. 34 0
      contributing.md
  3. 18 8
      src/interface.ts
  4. 30 0
      src/types.ts
  5. 9 35
      src/utils/xhr.ts

+ 1 - 0
changelog.md

@@ -50,6 +50,7 @@
   - Added functions:
     - `getAutoLikeData()` to return the current auto-like data (authenticated function)
     - `saveAutoLikeData()` to overwrite the auto-like data (authenticated function)
+    - `fetchVideoVotes()` to fetch the approximate like and dislike count of a video from [ReturnYoutubeDislikes](https://returnyoutubedislike.com/)
   - Added new SelectorObserver instance `browseResponse` for pages like `/channel/{id}`
   - Added library `compare-versions` to the plugin interface at `unsafeWindow.BYTM.compareVersions` for easier plugin version comparison
   - Added events

+ 34 - 0
contributing.md

@@ -340,6 +340,7 @@ Functions marked with 🔒 need to be passed a per-session and per-plugin authen
 - Auto-Like:
   - [getAutoLikeData()](#getautolikedata) 🔒 - Returns the current auto-like data object
   - [saveAutoLikeData()](#saveautolikedata) 🔒 - Overwrites the current auto-like data object with the provided one
+  - [fetchVideoVotes()](#fetchvideovotes) - Fetches the approximate like and dislike count for the video with the specified ID
 - Other:
   - [NanoEmitter](#nanoemitter) - Abstract class for creating lightweight, type safe event emitting classes
 
@@ -1092,6 +1093,39 @@ Functions marked with 🔒 need to be passed a per-session and per-plugin authen
 
 <br>
 
+> #### fetchVideoVotes()
+> Usage:
+> ```ts
+> unsafeWindow.BYTM.fetchVideoVotes(videoId: string): Promise<VideoVotesObj | undefined>
+> ```
+>   
+> Description:  
+> Fetches the approximate like and dislike counts for the specified video ID, using the [ReturnYoutubeDislike](https://returnyoutubedislike.com/) API.  
+> RYD will approximate the votes based on historical data and users of the browser extension. The numbers will never be 100% accurate!  
+> The object returned by this function has the structure of the `VideoVotesObj` type in the file [`src/types.ts`](src/types.ts)  
+> If the video ID is not found or the API is down, the function will return `undefined`  
+>   
+> Arguments:  
+> - `videoId` - The video ID to fetch the votes for (e.g. `dQw4w9WgXcQ`)
+> 
+> <details><summary><b>Example <i>(click to expand)</i></b></summary>
+> 
+> ```ts
+> async function getVotes() {
+>   const votes = await unsafeWindow.BYTM.fetchVideoVotes("dQw4w9WgXcQ");
+> 
+>   if(!votes)
+>     return console.error("Couldn't fetch the votes for this video");
+> 
+>   console.log(`The video has ${votes.likes} likes and ${votes.dislikes} dislikes`);
+> }
+> 
+> getVotes();
+> ```
+> </details>
+
+<br>
+
 > #### NanoEmitter
 > Usage:  
 > ```ts

+ 18 - 8
src/interface.ts

@@ -1,7 +1,7 @@
 import * as UserUtils from "@sv443-network/userutils";
 import * as compareVersions from "compare-versions";
 import { mode, branch, host, buildNumber, compressionFormat, scriptInfo } from "./constants.js";
-import { getResourceUrl, getSessionId, getVideoTime, log, setLocale, getLocale, hasKey, hasKeyFor, NanoEmitter, t, tp, type TrLocale, info, error, onInteraction, getThumbnailUrl, getBestThumbnailUrl } from "./utils/index.js";
+import { getResourceUrl, getSessionId, getVideoTime, log, setLocale, getLocale, hasKey, hasKeyFor, NanoEmitter, t, tp, type TrLocale, info, error, onInteraction, getThumbnailUrl, getBestThumbnailUrl, fetchVideoVotes } from "./utils/index.js";
 import { addSelectorListener } from "./observers.js";
 import { getFeatures, setFeatures } from "./config.js";
 import { autoLikeStore, featInfo, fetchLyricsUrlTop, getLyricsCacheEntry, sanitizeArtists, sanitizeSong } from "./features/index.js";
@@ -105,39 +105,49 @@ export const allInterfaceEvents = [
   ...allSiteEvents.map(e => `bytm:siteEvent:${e}`),
 ] as const;
 
-/** All functions that can be called on the BYTM interface using `unsafeWindow.BYTM.functionName();` (or `const { functionName } = unsafeWindow.BYTM;`) */
+/**
+ * All functions that can be called on the BYTM interface using `unsafeWindow.BYTM.functionName();` (or `const { functionName } = unsafeWindow.BYTM;`)  
+ * If prefixed with /**\/, the function is authenticated and requires a token to be passed as the first argument.
+ */
 const globalFuncs = {
   // meta:
   registerPlugin,
-  /**/getPluginInfo,
+  /**/ getPluginInfo,
 
   // bytm-specific:
   getResourceUrl,
   getSessionId,
+
   // dom:
   addSelectorListener,
   onInteraction,
   getVideoTime,
   getThumbnailUrl,
   getBestThumbnailUrl,
+
   // translations:
-  /**/setLocale: setLocaleInterface,
+  /**/ setLocale: setLocaleInterface,
   getLocale,
   hasKey,
   hasKeyFor,
   t,
   tp,
+
   // feature config:
-  /**/getFeatures: getFeaturesInterface,
-  /**/saveFeatures: saveFeaturesInterface,
+  /**/ getFeatures: getFeaturesInterface,
+  /**/ saveFeatures: saveFeaturesInterface,
+
   // lyrics:
   fetchLyricsUrlTop,
   getLyricsCacheEntry,
   sanitizeArtists,
   sanitizeSong,
+
   // auto-like:
-  /**/getAutoLikeData: getAutoLikeDataInterface,
-  /**/saveAutoLikeData: saveAutoLikeDataInterface,
+  /**/ getAutoLikeData: getAutoLikeDataInterface,
+  /**/ saveAutoLikeData: saveAutoLikeDataInterface,
+  fetchVideoVotes,
+
   // components:
   createHotkeyInput,
   createToggleInput,

+ 30 - 0
src/types.ts

@@ -64,6 +64,36 @@ export type AutoLikeData = {
   }[];
 };
 
+export type RYDVotesObj = {
+  /** The watch ID of the video */
+  id: string;
+  /** ISO timestamp of when the video was uploaded */
+  dateCreated: string;
+  /** Amount of likes */
+  likes: number;
+  /** Amount of dislikes */
+  dislikes: number;
+  /** Like to dislike ratio from 0.0 to 5.0 */
+  rating: number;
+  /** Amount of views */
+  viewCount: number;
+  /** Whether the video was deleted */
+  deleted: boolean;
+};
+
+export type VideoVotesObj = {
+  /** The watch ID of the video */
+  id: string;
+  /** Amount of likes */
+  likes: number;
+  /** Amount of dislikes */
+  dislikes: number;
+  /** Like to dislike ratio from 0.0 to 5.0 */
+  rating: number;
+  /** Timestamp of when the data was fetched */
+  timestamp: number;
+};
+
 //#region global
 
 // shim for the BYTM interface properties

+ 9 - 35
src/utils/xhr.ts

@@ -1,7 +1,7 @@
 import { fetchAdvanced, type Stringifiable } from "@sv443-network/userutils";
-import type { ResourceKey } from "../types.js";
+import type { RYDVotesObj, ResourceKey, VideoVotesObj } from "../types.js";
 import { getResourceUrl } from "./misc.js";
-import { error } from "./logging.js";
+import { error, info } from "./logging.js";
 
 /**
  * Constructs a URL from a base URL and a record of query parameters.  
@@ -57,36 +57,6 @@ export async function fetchCss(key: ResourceKey & `css-${string}`) {
   }
 }
 
-export type ReturnYouTubeDislikeVotes = {
-  /** The watch ID of the video */
-  id: string;
-  /** ISO timestamp of when the video was uploaded */
-  dateCreated: string;
-  /** Amount of likes */
-  likes: number;
-  /** Amount of dislikes */
-  dislikes: number;
-  /** Like to dislike ratio from 0.0 to 5.0 */
-  rating: number;
-  /** Amount of views */
-  viewCount: number;
-  /** Whether the video was deleted */
-  deleted: boolean;
-};
-
-export type VideoVotesObj = {
-  /** The watch ID of the video */
-  id: string;
-  /** Amount of likes */
-  likes: number;
-  /** Amount of dislikes */
-  dislikes: number;
-  /** Like to dislike ratio from 0.0 to 5.0 */
-  rating: number;
-  /** Timestamp of when the data was fetched */
-  timestamp: number;
-};
-
 /** Cache for the vote data of YouTube videos to prevent unnecessary requests */
 const voteCache = new Map<string, VideoVotesObj>();
 /** Time-to-live for the vote cache in milliseconds */
@@ -96,12 +66,14 @@ const voteCacheTTL = 1000 * 60 * 5;
  * Fetches the votes object for a YouTube video from the [Return YouTube Dislike API.](https://returnyoutubedislike.com/docs)
  * @param watchId The watch ID of the video
  */
-export async function fetchVideoVotes(watchId: string) {
+export async function fetchVideoVotes(watchId: string): Promise<VideoVotesObj | undefined> {
   try {
     if(voteCache.has(watchId)) {
       const cached = voteCache.get(watchId)!;
-      if(Date.now() - cached.timestamp < voteCacheTTL)
+      if(Date.now() - cached.timestamp < voteCacheTTL) {
+        info(`Returning cached video votes for watch ID '${watchId}':`, cached);
         return cached;
+      }
       else
         voteCache.delete(watchId);
     }
@@ -111,7 +83,7 @@ export async function fetchVideoVotes(watchId: string) {
         method: "GET",
         url: `https://returnyoutubedislikeapi.com/votes?videoId=${watchId}`,
       })).response
-    ) as ReturnYouTubeDislikeVotes;
+    ) as RYDVotesObj;
 
     if(!("id" in votesRaw) || !("likes" in votesRaw) || !("dislikes" in votesRaw) || !("rating" in votesRaw)) {
       error("Couldn't parse video votes due to an error:", votesRaw);
@@ -127,6 +99,8 @@ export async function fetchVideoVotes(watchId: string) {
     };
     voteCache.set(votesObj.id, votesObj);
 
+    info(`Fetched video votes for watch ID '${watchId}':`, votesObj);
+
     return votesObj;
   }
   catch(err) {