-
-
Notifications
You must be signed in to change notification settings - Fork 54
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
cache sponsorblock segments in redis (#1482)
- Loading branch information
Showing
5 changed files
with
126 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,20 +1,64 @@ | ||
import { SponsorBlock } from "sponsorblock-api"; | ||
import { SponsorBlock, type Segment } from "sponsorblock-api"; | ||
import { redisClient } from "./redisclient"; | ||
import { v4 as uuidv4 } from "uuid"; | ||
import { ALL_SKIP_CATEGORIES } from "ott-common"; | ||
import { conf } from "./ott-config"; | ||
import { getLogger } from "./logger"; | ||
|
||
const log = getLogger("sponsorblock"); | ||
|
||
const SPONSORBLOCK_USERID_KEY = `sponsorblock-userid`; | ||
const SEGMENT_CACHE_PREFIX = "segments"; | ||
|
||
let _cachedUserId: string | null = null; | ||
|
||
export async function getSponsorBlockUserId(): Promise<string> { | ||
if (_cachedUserId) { | ||
return _cachedUserId; | ||
} | ||
let userid = await redisClient.get(SPONSORBLOCK_USERID_KEY); | ||
if (!userid) { | ||
userid = uuidv4(); | ||
await redisClient.set(SPONSORBLOCK_USERID_KEY, userid); | ||
} | ||
_cachedUserId = userid; | ||
return userid; | ||
} | ||
|
||
/** Used for tests. */ | ||
export function clearUserId() { | ||
_cachedUserId = null; | ||
} | ||
|
||
export async function getSponsorBlock(): Promise<SponsorBlock> { | ||
const userid = await getSponsorBlockUserId(); | ||
const sponsorblock = new SponsorBlock(userid); | ||
return sponsorblock; | ||
} | ||
|
||
export async function fetchSegments(videoId: string): Promise<Segment[]> { | ||
if (conf.get("video.sponsorblock.cache_ttl") > 0) { | ||
const cachedSegments = await redisClient.get(`${SEGMENT_CACHE_PREFIX}:${videoId}`); | ||
if (cachedSegments) { | ||
try { | ||
return JSON.parse(cachedSegments); | ||
} catch (e) { | ||
log.warn( | ||
`Failed to parse cached segments for video ${videoId}, fetching fresh segments` | ||
); | ||
} | ||
} | ||
} | ||
const sponsorblock = await getSponsorBlock(); | ||
const segments = await sponsorblock.getSegments(videoId, ALL_SKIP_CATEGORIES); | ||
await cacheSegments(videoId, segments); | ||
return segments; | ||
} | ||
|
||
async function cacheSegments(videoId: string, segments: Segment[]) { | ||
await redisClient.setEx( | ||
`${SEGMENT_CACHE_PREFIX}:${videoId}`, | ||
conf.get("video.sponsorblock.cache_ttl"), | ||
JSON.stringify(segments) | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach } from "vitest"; | ||
import { redisClient, buildClients } from "../../redisclient"; | ||
import * as sponsorblock from "../../sponsorblock"; | ||
import { SponsorBlock } from "sponsorblock-api"; | ||
|
||
vi.mock("sponsorblock-api", () => { | ||
return { | ||
SponsorBlock: class { | ||
getSegments = vi.fn().mockResolvedValue([{ start: 0, end: 1, category: "sponsor" }]); | ||
}, | ||
}; | ||
}); | ||
|
||
describe("SponsorBlock", () => { | ||
beforeAll(async () => { | ||
await buildClients(); | ||
}); | ||
|
||
it("should generate and return the same user id and only ping redis once", async () => { | ||
sponsorblock.clearUserId(); | ||
let getSpy = vi.spyOn(redisClient, "get").mockResolvedValue(null); | ||
let setSpy = vi.spyOn(redisClient, "set"); | ||
const { getSponsorBlockUserId } = sponsorblock; | ||
const userId = await getSponsorBlockUserId(); | ||
expect(userId).toBeDefined(); | ||
const userId2 = await getSponsorBlockUserId(); | ||
expect(userId2).toEqual(userId); | ||
|
||
expect(redisClient.get).toHaveBeenCalledTimes(1); | ||
expect(redisClient.set).toHaveBeenCalledTimes(1); | ||
getSpy.mockRestore(); | ||
setSpy.mockRestore(); | ||
}); | ||
|
||
it("should always return the same user id and only ping redis once", async () => { | ||
sponsorblock.clearUserId(); | ||
let getSpy = vi.spyOn(redisClient, "get").mockResolvedValue("testuserid"); | ||
let setSpy = vi.spyOn(redisClient, "set"); | ||
const { getSponsorBlockUserId } = sponsorblock; | ||
const userId = await getSponsorBlockUserId(); | ||
expect(userId).toBe("testuserid"); | ||
const userId2 = await getSponsorBlockUserId(); | ||
expect(userId2).toBe("testuserid"); | ||
|
||
expect(redisClient.get).toHaveBeenCalledTimes(1); | ||
expect(redisClient.set).not.toHaveBeenCalled(); | ||
getSpy.mockRestore(); | ||
setSpy.mockRestore(); | ||
}); | ||
|
||
it("should fetch new segments if parsing cached entry fails", async () => { | ||
let getSpy = vi.spyOn(redisClient, "get").mockResolvedValue("[invalid json"); | ||
let setSpy = vi.spyOn(redisClient, "setEx"); | ||
const { fetchSegments } = sponsorblock; | ||
const segments = await fetchSegments("testvideo"); | ||
expect(segments).toEqual([{ start: 0, end: 1, category: "sponsor" }]); | ||
expect(redisClient.get).toHaveBeenCalledTimes(1); | ||
expect(redisClient.setEx).toHaveBeenCalledTimes(1); | ||
getSpy.mockRestore(); | ||
setSpy.mockRestore(); | ||
}); | ||
}); |