Skip to content

Commit b6d2cdd

Browse files
committed
fix(cloudstream): update catalog and search endpoints to return provider-scoped sb_ IDs
1 parent e9418dc commit b6d2cdd

2 files changed

Lines changed: 75 additions & 22 deletions

File tree

backend/src/api/cloudstreamRoutes.js

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ const { touchUserLastSeen } = require('../utils/userActivity');
2323

2424
const router = Router();
2525

26-
const TMDB_POSTER_BASE = 'https://image.tmdb.org/t/p/w500';
27-
2826
// ─── Auth Helper ──────────────────────────────────────────────────────────────
2927

3028
/**
@@ -81,27 +79,14 @@ function toStremioType(csType) {
8179

8280
/**
8381
* Build a lightweight CloudStream search/catalog result item from a VOD row.
84-
* Mirrors the ID logic in addonHandler's buildMetaPreview().
82+
* CloudStream should stay provider-scoped, so it always uses the internal
83+
* sb_<id> form rather than TMDB/IMDb IDs.
8584
*/
8685
function buildCSItem(item) {
87-
// Live channels are resolved per-provider row (not via TMDB), so always use
88-
// the sb_<id> form for them. Movies/series prefer the TMDB/IMDB ID so
89-
// handleMeta can aggregate streams across providers.
90-
const isLive = item.vod_type === 'live';
91-
const hasMatch = !isLive && item.tmdb_id != null;
92-
93-
const url = hasMatch
94-
? (item.imdb_id || `tmdb:${item.tmdb_id}`)
95-
: `sb_${item.id}`;
96-
97-
const posterUrl = hasMatch && item.poster_path
98-
? `${TMDB_POSTER_BASE}${item.poster_path}`
99-
: (item.poster_url || null);
100-
10186
return {
10287
name: item.raw_title || 'Unknown',
103-
url,
104-
posterUrl,
88+
url: `sb_${item.id}`,
89+
posterUrl: item.poster_url || null,
10590
type: toCSType(item.vod_type),
10691
year: item.title_year || null,
10792
tags: item.category ? [item.category] : [],
@@ -367,13 +352,13 @@ router.get('/search', async (req, res) => {
367352

368353
/**
369354
* Returns full detail for a single item, including the episode list for series.
370-
* Delegates to addonHandler.handleMeta() so it reuses all caching, TMDB
371-
* enrichment, and EPG logic already in place.
355+
* Delegates to addonHandler.handleMeta() using the provider-scoped sb_<id>
356+
* identifier emitted by CloudStream catalog/search responses.
372357
*
373358
* Query params:
374359
* token – addon token (required)
375360
* url – content ID from a catalog/search result (required)
376-
* type – 'Movie' | 'TvSeries' | 'Live' (required for correct TMDB lookup)
361+
* type – 'Movie' | 'TvSeries' | 'Live'
377362
*
378363
* Response:
379364
* { name, url, posterUrl, type, year, plot, tags, episodes? }

backend/tests/integration/api.test.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,6 +505,74 @@ describe('GET /addon/:token/stream/:type/:id.json', () => {
505505
});
506506
});
507507

508+
describe('GET /cloudstream/catalog', () => {
509+
it('returns provider-scoped sb_ ids for matched items', async () => {
510+
const { providerQueries, vodQueries } = require('../../src/db/queries');
511+
512+
providerQueries.findByIdAndUser.mockResolvedValueOnce({ id: 'prov-1', name: 'Provider 1' });
513+
vodQueries.getByProvider.mockResolvedValueOnce([
514+
{
515+
id: '1653512',
516+
raw_title: 'Matched Movie',
517+
vod_type: 'movie',
518+
tmdb_id: 1653512,
519+
imdb_id: 'tt1234567',
520+
poster_url: 'https://img.test/movie.jpg',
521+
title_year: 2026,
522+
category: 'Action',
523+
},
524+
]);
525+
vodQueries.countByProvider.mockResolvedValueOnce(1);
526+
527+
const res = await request(app)
528+
.get('/cloudstream/catalog')
529+
.query({ token: 'abc123token', providerId: 'prov-1', type: 'Movie' });
530+
531+
expect(res.status).toBe(200);
532+
expect(res.body.results).toEqual([
533+
expect.objectContaining({
534+
url: 'sb_1653512',
535+
name: 'Matched Movie',
536+
posterUrl: 'https://img.test/movie.jpg',
537+
}),
538+
]);
539+
});
540+
});
541+
542+
describe('GET /cloudstream/search', () => {
543+
it('returns provider-scoped sb_ ids for matched search results', async () => {
544+
const { providerQueries, vodQueries } = require('../../src/db/queries');
545+
546+
providerQueries.findByUser.mockResolvedValueOnce([{ id: 'prov-1', name: 'Provider 1' }]);
547+
vodQueries.getByProvider.mockResolvedValueOnce([
548+
{
549+
id: 'series-42',
550+
raw_title: 'Matched Series',
551+
vod_type: 'series',
552+
tmdb_id: 42,
553+
imdb_id: 'tt7654321',
554+
poster_url: 'https://img.test/series.jpg',
555+
title_year: 2025,
556+
category: 'Drama',
557+
},
558+
]);
559+
560+
const res = await request(app)
561+
.get('/cloudstream/search')
562+
.query({ token: 'abc123token', query: 'matched', type: 'TvSeries' });
563+
564+
expect(res.status).toBe(200);
565+
expect(res.body.results).toEqual([
566+
expect.objectContaining({
567+
url: 'sb_series-42',
568+
name: 'Matched Series',
569+
posterUrl: 'https://img.test/series.jpg',
570+
type: 'TvSeries',
571+
}),
572+
]);
573+
});
574+
});
575+
508576
// ─── Admin Routes ─────────────────────────────────────────────────────────────
509577

510578
describe('POST /admin/auth/login', () => {

0 commit comments

Comments
 (0)