feat: KSPlayer as an option for iOS + other improvements (#1266)

This commit is contained in:
Fredrik Burmester
2026-01-03 13:05:50 +01:00
committed by GitHub
parent d1795c9df8
commit 74d86b5d12
191 changed files with 88479 additions and 2316 deletions

319
utils/streamystats/api.ts Normal file
View File

@@ -0,0 +1,319 @@
import axios from "axios";
import type {
AddWatchlistItemResponse,
CreateWatchlistRequest,
CreateWatchlistResponse,
DeleteWatchlistResponse,
GetWatchlistItemsParams,
GetWatchlistsResponse,
RemoveWatchlistItemResponse,
StreamystatsRecommendationsFullResponse,
StreamystatsRecommendationsIdsResponse,
StreamystatsRecommendationsParams,
StreamystatsSearchFullResponse,
StreamystatsSearchIdsResponse,
StreamystatsSearchParams,
StreamystatsWatchlistDetailFullResponse,
StreamystatsWatchlistDetailIdsResponse,
StreamystatsWatchlistDetailParams,
StreamystatsWatchlistsFullResponse,
StreamystatsWatchlistsParams,
UpdateWatchlistRequest,
UpdateWatchlistResponse,
} from "./types";
interface StreamystatsApiConfig {
serverUrl: string;
jellyfinToken: string;
}
export const createStreamystatsApi = (config: StreamystatsApiConfig) => {
const { serverUrl, jellyfinToken } = config;
const baseUrl = serverUrl.endsWith("/") ? serverUrl.slice(0, -1) : serverUrl;
const headers = {
Authorization: `MediaBrowser Token="${jellyfinToken}"`,
};
const search = async (
params: StreamystatsSearchParams,
): Promise<
StreamystatsSearchIdsResponse | StreamystatsSearchFullResponse
> => {
const queryParams = new URLSearchParams();
queryParams.set("q", params.q);
if (params.limit) {
queryParams.set("limit", params.limit.toString());
}
if (params.format) {
queryParams.set("format", params.format);
}
if (params.type) {
queryParams.set("type", params.type);
}
const url = `${baseUrl}/api/search?${queryParams.toString()}`;
const response = await axios.get(url, { headers });
return response.data;
};
const searchIds = async (
query: string,
type?: StreamystatsSearchParams["type"],
limit?: number,
): Promise<StreamystatsSearchIdsResponse> => {
return search({
q: query,
format: "ids",
type,
limit,
}) as Promise<StreamystatsSearchIdsResponse>;
};
const searchFull = async (
query: string,
type?: StreamystatsSearchParams["type"],
limit?: number,
): Promise<StreamystatsSearchFullResponse> => {
return search({
q: query,
format: "full",
type,
limit,
}) as Promise<StreamystatsSearchFullResponse>;
};
const getRecommendations = async (
params: StreamystatsRecommendationsParams,
): Promise<
| StreamystatsRecommendationsIdsResponse
| StreamystatsRecommendationsFullResponse
> => {
const queryParams = new URLSearchParams();
if (params.serverId) {
queryParams.set("serverId", params.serverId.toString());
}
if (params.serverName) {
queryParams.set("serverName", params.serverName);
}
if (params.jellyfinServerId) {
queryParams.set("jellyfinServerId", params.jellyfinServerId);
}
if (params.limit) {
queryParams.set("limit", params.limit.toString());
}
if (params.type) {
queryParams.set("type", params.type);
}
if (params.range) {
queryParams.set("range", params.range);
}
if (params.format) {
queryParams.set("format", params.format);
}
if (params.includeBasedOn !== undefined) {
queryParams.set("includeBasedOn", params.includeBasedOn.toString());
}
if (params.includeReasons !== undefined) {
queryParams.set("includeReasons", params.includeReasons.toString());
}
const url = `${baseUrl}/api/recommendations?${queryParams.toString()}`;
const response = await axios.get(url, { headers });
return response.data;
};
const getRecommendationIds = async (
jellyfinServerId: string,
type?: StreamystatsRecommendationsParams["type"],
limit?: number,
): Promise<StreamystatsRecommendationsIdsResponse> => {
return getRecommendations({
jellyfinServerId,
format: "ids",
type,
limit,
includeBasedOn: false,
includeReasons: false,
}) as Promise<StreamystatsRecommendationsIdsResponse>;
};
const getPromotedWatchlists = async (
params: StreamystatsWatchlistsParams,
): Promise<StreamystatsWatchlistsFullResponse> => {
const queryParams = new URLSearchParams();
if (params.serverId) {
queryParams.set("serverId", params.serverId.toString());
}
if (params.serverName) {
queryParams.set("serverName", params.serverName);
}
if (params.serverUrl) {
queryParams.set("serverUrl", params.serverUrl);
}
if (params.jellyfinServerId) {
queryParams.set("jellyfinServerId", params.jellyfinServerId);
}
if (params.limit) {
queryParams.set("limit", params.limit.toString());
}
if (params.format) {
queryParams.set("format", params.format);
}
if (params.includePreview !== undefined) {
queryParams.set("includePreview", params.includePreview.toString());
}
const url = `${baseUrl}/api/watchlists/promoted?${queryParams.toString()}`;
const response = await axios.get(url, { headers });
return response.data;
};
const getWatchlistItemIds = async (
params: StreamystatsWatchlistDetailParams,
): Promise<StreamystatsWatchlistDetailIdsResponse> => {
const queryParams = new URLSearchParams();
queryParams.set("format", "ids");
if (params.serverId) {
queryParams.set("serverId", params.serverId.toString());
}
if (params.serverName) {
queryParams.set("serverName", params.serverName);
}
if (params.serverUrl) {
queryParams.set("serverUrl", params.serverUrl);
}
if (params.jellyfinServerId) {
queryParams.set("jellyfinServerId", params.jellyfinServerId);
}
const url = `${baseUrl}/api/watchlists/${params.watchlistId}?${queryParams.toString()}`;
const response = await axios.get(url, { headers });
return response.data;
};
/**
* Get all watchlists (own + public)
* GET /api/watchlists
*/
const getWatchlists = async (): Promise<GetWatchlistsResponse> => {
const url = `${baseUrl}/api/watchlists`;
const response = await axios.get(url, { headers });
return response.data;
};
/**
* Create a new watchlist
* POST /api/watchlists
*/
const createWatchlist = async (
data: CreateWatchlistRequest,
): Promise<CreateWatchlistResponse> => {
const url = `${baseUrl}/api/watchlists`;
const response = await axios.post(url, data, { headers });
return response.data;
};
/**
* Get a single watchlist with items
* GET /api/watchlists/[id]
*/
const getWatchlistDetail = async (
watchlistId: number,
params?: GetWatchlistItemsParams,
): Promise<StreamystatsWatchlistDetailFullResponse> => {
const queryParams = new URLSearchParams();
queryParams.set("format", "full");
if (params?.type) {
queryParams.set("type", params.type);
}
if (params?.sort) {
queryParams.set("sort", params.sort);
}
const url = `${baseUrl}/api/watchlists/${watchlistId}?${queryParams.toString()}`;
const response = await axios.get(url, { headers });
return response.data;
};
/**
* Update a watchlist (owner only)
* PATCH /api/watchlists/[id]
*/
const updateWatchlist = async (
watchlistId: number,
data: UpdateWatchlistRequest,
): Promise<UpdateWatchlistResponse> => {
const url = `${baseUrl}/api/watchlists/${watchlistId}`;
const response = await axios.patch(url, data, { headers });
return response.data;
};
/**
* Delete a watchlist (owner only)
* DELETE /api/watchlists/[id]
*/
const deleteWatchlist = async (
watchlistId: number,
): Promise<DeleteWatchlistResponse> => {
const url = `${baseUrl}/api/watchlists/${watchlistId}`;
const response = await axios.delete(url, { headers });
return response.data;
};
/**
* Add an item to a watchlist (owner only)
* POST /api/watchlists/[id]/items
*/
const addWatchlistItem = async (
watchlistId: number,
itemId: string,
): Promise<AddWatchlistItemResponse> => {
const url = `${baseUrl}/api/watchlists/${watchlistId}/items`;
const response = await axios.post(url, { itemId }, { headers });
return response.data;
};
/**
* Remove an item from a watchlist (owner only)
* DELETE /api/watchlists/[id]/items/[itemId]
*/
const removeWatchlistItem = async (
watchlistId: number,
itemId: string,
): Promise<RemoveWatchlistItemResponse> => {
const url = `${baseUrl}/api/watchlists/${watchlistId}/items/${itemId}`;
const response = await axios.delete(url, { headers });
return response.data;
};
return {
search,
searchIds,
searchFull,
getRecommendations,
getRecommendationIds,
getPromotedWatchlists,
getWatchlistItemIds,
// Watchlist CRUD
getWatchlists,
createWatchlist,
getWatchlistDetail,
updateWatchlist,
deleteWatchlist,
addWatchlistItem,
removeWatchlistItem,
};
};
export type StreamystatsApi = ReturnType<typeof createStreamystatsApi>;

View File

@@ -0,0 +1,2 @@
export * from "./api";
export * from "./types";

329
utils/streamystats/types.ts Normal file
View File

@@ -0,0 +1,329 @@
/**
* Streamystats Search API Types
* Based on the Search API specification
*/
export type StreamystatsSearchType =
| "all"
| "media"
| "movies"
| "series"
| "episodes"
| "audio"
| "people"
| "actors"
| "directors"
| "writers"
| "users"
| "watchlists"
| "activities"
| "sessions";
export type StreamystatsSearchFormat = "full" | "ids";
export interface StreamystatsSearchParams {
q: string;
limit?: number;
format?: StreamystatsSearchFormat;
type?: StreamystatsSearchType;
}
export interface StreamystatsSearchResultItem {
id: string;
type: "item" | "user" | "watchlist" | "activity" | "session" | "actor";
subtype?: string;
title: string;
subtitle?: string;
imageId?: string;
imageTag?: string;
href?: string;
rank?: number;
metadata?: Record<string, unknown>;
}
export interface StreamystatsSearchFullResponse {
data: {
items: StreamystatsSearchResultItem[];
users: StreamystatsSearchResultItem[];
watchlists: StreamystatsSearchResultItem[];
activities: StreamystatsSearchResultItem[];
sessions: StreamystatsSearchResultItem[];
actors: StreamystatsSearchResultItem[];
total: number;
};
error?: string;
}
export interface StreamystatsSearchIdsResponse {
data: {
movies: string[];
series: string[];
episodes: string[];
seasons: string[];
audio: string[];
actors: string[];
directors: string[];
writers: string[];
total: number;
};
error?: string;
}
export type StreamystatsSearchResponse =
| StreamystatsSearchFullResponse
| StreamystatsSearchIdsResponse;
/**
* Streamystats Recommendations API Types
*/
export type StreamystatsRecommendationType = "Movie" | "Series" | "all";
export type StreamystatsRecommendationRange =
| "7d"
| "30d"
| "90d"
| "thisMonth"
| "all";
export interface StreamystatsRecommendationsParams {
serverId?: number;
serverName?: string;
jellyfinServerId?: string;
limit?: number;
type?: StreamystatsRecommendationType;
range?: StreamystatsRecommendationRange;
format?: StreamystatsSearchFormat;
includeBasedOn?: boolean;
includeReasons?: boolean;
}
export interface StreamystatsRecommendationItem {
id: string;
name: string;
type: "Movie" | "Series";
primaryImageTag?: string;
backdropImageTag?: string;
overview?: string;
year?: number;
}
export interface StreamystatsRecommendation {
item: StreamystatsRecommendationItem;
similarity: number;
basedOn?: StreamystatsRecommendationItem[];
reason?: string;
}
export interface StreamystatsRecommendationsFullResponse {
server: {
id: number;
name: string;
};
user: {
id: string;
name: string;
};
params: Record<string, unknown>;
data: StreamystatsRecommendation[];
error?: string;
}
export interface StreamystatsRecommendationsIdsResponse {
data: {
movies: string[];
series: string[];
total: number;
};
error?: string;
}
export type StreamystatsRecommendationsResponse =
| StreamystatsRecommendationsFullResponse
| StreamystatsRecommendationsIdsResponse;
/**
* Streamystats Watchlists API Types
*/
export interface StreamystatsServerInfo {
id: number;
name: string;
}
export interface StreamystatsWatchlistsParams {
serverId?: number;
serverName?: string;
serverUrl?: string;
jellyfinServerId?: string;
limit?: number;
format?: StreamystatsSearchFormat;
includePreview?: boolean;
}
export interface StreamystatsWatchlistPreviewItem {
id: string;
name: string;
type: "Movie" | "Series" | "Episode";
primaryImageTag?: string;
}
export interface StreamystatsWatchlistItem {
id: string;
name: string;
type: "Movie" | "Series" | "Episode";
productionYear?: number;
runtimeTicks?: number;
genres?: string[];
primaryImageTag?: string;
seriesId?: string;
seriesName?: string;
communityRating?: number;
}
export interface StreamystatsWatchlistItemEntry {
id: number;
watchlistId: number;
itemId: string;
position: number;
addedAt: string;
item: StreamystatsWatchlistItem;
}
export interface StreamystatsWatchlist {
id: number;
serverId: number;
userId: string;
name: string;
description?: string | null;
isPublic: boolean;
isPromoted: boolean;
allowedItemType?: string;
defaultSortOrder?: string;
createdAt: string;
updatedAt: string;
itemCount?: number;
previewItems?: StreamystatsWatchlistPreviewItem[];
items?: StreamystatsWatchlistItemEntry[];
}
export interface StreamystatsWatchlistsFullResponse {
server: StreamystatsServerInfo;
data: StreamystatsWatchlist[];
total: number;
error?: string;
}
export interface StreamystatsWatchlistsIdsResponse {
data: {
watchlists: string[];
total: number;
};
error?: string;
}
export type StreamystatsWatchlistsResponse =
| StreamystatsWatchlistsFullResponse
| StreamystatsWatchlistsIdsResponse;
export interface StreamystatsWatchlistDetailParams {
watchlistId: number;
serverId?: number;
serverName?: string;
serverUrl?: string;
jellyfinServerId?: string;
format?: StreamystatsSearchFormat;
}
export interface StreamystatsWatchlistDetailFullResponse {
server: StreamystatsServerInfo;
data: StreamystatsWatchlist;
error?: string;
}
export interface StreamystatsWatchlistDetailIdsResponse {
server: StreamystatsServerInfo;
data: {
id: number;
name: string;
items: string[];
};
error?: string;
}
export type StreamystatsWatchlistDetailResponse =
| StreamystatsWatchlistDetailFullResponse
| StreamystatsWatchlistDetailIdsResponse;
/**
* Streamystats Watchlists CRUD Types
*/
export type StreamystatsWatchlistAllowedItemType =
| "Movie"
| "Series"
| "Episode"
| null;
export type StreamystatsWatchlistSortOrder =
| "custom"
| "name"
| "dateAdded"
| "releaseDate";
export interface CreateWatchlistRequest {
name: string;
description?: string;
isPublic?: boolean;
allowedItemType?: StreamystatsWatchlistAllowedItemType;
defaultSortOrder?: StreamystatsWatchlistSortOrder;
}
export interface UpdateWatchlistRequest {
name?: string;
description?: string;
isPublic?: boolean;
allowedItemType?: StreamystatsWatchlistAllowedItemType;
defaultSortOrder?: StreamystatsWatchlistSortOrder;
}
export interface CreateWatchlistResponse {
data: StreamystatsWatchlist;
error?: string;
}
export interface UpdateWatchlistResponse {
data: StreamystatsWatchlist;
error?: string;
}
export interface DeleteWatchlistResponse {
success: boolean;
error?: string;
}
export interface AddWatchlistItemResponse {
data: {
id: number;
watchlistId: number;
itemId: string;
position: number;
addedAt: string;
};
error?: string;
}
export interface RemoveWatchlistItemResponse {
success: boolean;
error?: string;
}
export interface GetWatchlistsResponse {
data: StreamystatsWatchlist[];
error?: string;
}
export interface GetWatchlistItemsParams {
type?: "Movie" | "Series" | "Episode";
sort?: StreamystatsWatchlistSortOrder;
}