mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-07-16 09:23:07 +01:00
feat: KSPlayer as an option for iOS + other improvements (#1266)
This commit is contained in:
committed by
GitHub
parent
d1795c9df8
commit
74d86b5d12
319
utils/streamystats/api.ts
Normal file
319
utils/streamystats/api.ts
Normal 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>;
|
||||
2
utils/streamystats/index.ts
Normal file
2
utils/streamystats/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./api";
|
||||
export * from "./types";
|
||||
329
utils/streamystats/types.ts
Normal file
329
utils/streamystats/types.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user