mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
Ability to set a country, both in server and user settings, to have the correct streaming provider (but doesn't affect language) (#463)
* Country support for watch providers (#1, not functional) * frontend dropdowns now actually read and write the correct setting * user country setting now respects the server side default when creating a new user * watch providers correctly show based on country, but changing the coutnry requires restarting the container to take effect * flushing cache when updating user settings * non functional, initial support for getting all the countries of which tmdb has streaming providers for to let users pick one * users get shown a list of country codes to pick from and the selection gets saved and used, but the dropdown menu doesn't allow for scrolling * limited dropdown height and auto scroll to the letter the user hits * simplified auto scroll code * the dropdown to choose countries now shows their full english name * server/user region improvements * DropDown: remove console log used to test * DropDown: Add keypress event listener on main div instead of document body, so only triggered when key pressed whilst focused on dropdown. --------- Co-authored-by: Stig <stig.narnia@gmail.com> Co-authored-by: IRHM <37304121+IRHM@users.noreply.github.com>
This commit is contained in:
@@ -89,6 +89,8 @@ type UserSettings struct {
|
||||
// even if the watched item state has since been changed.
|
||||
// Also if user wants to show in watched stats.
|
||||
IncludePreviouslyWatched *bool `gorm:"default:false" json:"includePreviouslyWatched"`
|
||||
// User's country to get correct content streaming providers.
|
||||
Country *string `gorm:"default:'US'" json:"country"`
|
||||
}
|
||||
|
||||
// Holds third party service auth tokens for users.
|
||||
@@ -291,6 +293,8 @@ func register(ur *UserRegisterRequest, initialPerm int, db *gorm.DB) (AuthRespon
|
||||
user.Permissions = initialPerm
|
||||
}
|
||||
|
||||
user.Country = &Config.DEFAULT_COUNTRY
|
||||
|
||||
res := db.Create(&user)
|
||||
if res.Error != nil {
|
||||
// If error is because unique contraint failed.. user already exists
|
||||
@@ -422,6 +426,7 @@ func loginJellyfin(user *User, db *gorm.DB) (AuthResponse, error) {
|
||||
dbUser.ThirdPartyAuth = resp.AccessToken
|
||||
dbUser.Username = resp.User.Name
|
||||
dbUser.Type = JELLYFIN_USER
|
||||
dbUser.Country = &Config.DEFAULT_COUNTRY
|
||||
|
||||
dbRes = db.Create(&dbUser)
|
||||
if dbRes.Error != nil {
|
||||
@@ -484,6 +489,7 @@ func loginPlex(lr *PlexLoginRequest, db *gorm.DB) (AuthResponse, error) {
|
||||
AuthToken: lr.AuthToken,
|
||||
AuthToken2: homeAuthToken,
|
||||
})
|
||||
dbUser.Country = &Config.DEFAULT_COUNTRY
|
||||
dbRes = db.Create(&dbUser)
|
||||
if dbRes.Error != nil {
|
||||
slog.Error("loginPlex: Failed to create new user in db from plex response", "error", dbRes.Error)
|
||||
|
||||
+9
-1
@@ -26,6 +26,10 @@ type ServerConfig struct {
|
||||
// it strong, just like a very long, complicated password.
|
||||
JWT_SECRET string `json:",omitempty"`
|
||||
|
||||
// Default country for new users. This is used to set the default
|
||||
// region to get correct content streaming providers.
|
||||
DEFAULT_COUNTRY string `json:",omitempty"`
|
||||
|
||||
// Optional: Point to your Jellyfin install
|
||||
// to enable it as an auth provider.
|
||||
JELLYFIN_HOST string `json:",omitempty"`
|
||||
@@ -67,6 +71,7 @@ type ServerConfig struct {
|
||||
func (c *ServerConfig) GetSafe() ServerConfig {
|
||||
return ServerConfig{
|
||||
SIGNUP_ENABLED: c.SIGNUP_ENABLED,
|
||||
DEFAULT_COUNTRY: c.DEFAULT_COUNTRY,
|
||||
JELLYFIN_HOST: c.JELLYFIN_HOST,
|
||||
TMDB_KEY: c.TMDB_KEY,
|
||||
PLEX_HOST: c.PLEX_HOST,
|
||||
@@ -126,7 +131,8 @@ func generateConfig() error {
|
||||
cfg := ServerConfig{
|
||||
JWT_SECRET: key,
|
||||
// Other defaults..
|
||||
SIGNUP_ENABLED: true,
|
||||
DEFAULT_COUNTRY: "US",
|
||||
SIGNUP_ENABLED: true,
|
||||
}
|
||||
barej, err := json.MarshalIndent(cfg, "", "\t")
|
||||
if err != nil {
|
||||
@@ -151,6 +157,8 @@ func updateConfig(k string, v any) error {
|
||||
} else if k == "DEBUG" {
|
||||
Config.DEBUG = v.(bool)
|
||||
setLoggingLevel()
|
||||
} else if k == "DEFAULT_COUNTRY" {
|
||||
Config.DEFAULT_COUNTRY = v.(string)
|
||||
} else {
|
||||
return errors.New("invalid setting")
|
||||
}
|
||||
|
||||
@@ -377,3 +377,13 @@ func upcomingTv() (TMDBUpcomingShows, error) {
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
func regions() (TMDBRegions, error) {
|
||||
resp := new(TMDBRegions)
|
||||
err := tmdbRequest("/watch/providers/regions", map[string]string{}, &resp)
|
||||
if err != nil {
|
||||
slog.Error("Failed to complete regions request!", "error", err.Error())
|
||||
return TMDBRegions{}, errors.New("failed to complete regions request")
|
||||
}
|
||||
return *resp, nil
|
||||
}
|
||||
|
||||
+17
-6
@@ -8,13 +8,24 @@ import (
|
||||
|
||||
// Location middleware
|
||||
func WhereaboutsRequired() gin.HandlerFunc {
|
||||
// TODO: This should also take into account a user
|
||||
// preference (of location.. which also needs to be
|
||||
// added) and server preference (env var). The US is
|
||||
// good enough for now as a default.
|
||||
return func(c *gin.Context) {
|
||||
slog.Debug("WhereaboutsRequired middleware hit", "country", "US")
|
||||
c.Set("userCountry", "US")
|
||||
region := c.Query("region")
|
||||
slog.Debug("WhereaboutsRequired: middleware hit", "region", region)
|
||||
if region == "" {
|
||||
// If no region is passed, default to server region.
|
||||
if Config.DEFAULT_COUNTRY != "" {
|
||||
slog.Debug("WhereaboutsRequired: Using server default country.", "default_country", Config.DEFAULT_COUNTRY)
|
||||
c.Set("userCountry", Config.DEFAULT_COUNTRY)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
// If no server region set, default to US.
|
||||
slog.Debug("WhereaboutsRequired: Using hard coded default (US).")
|
||||
c.Set("userCountry", "US")
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
c.Set("userCountry", region)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
+13
-7
@@ -94,7 +94,7 @@ func (b *BaseRouter) addContentRoutes() {
|
||||
}))
|
||||
|
||||
// Get movie details (for movie page)
|
||||
content.Use(WhereaboutsRequired()).GET("/movie/:id", cache.CachePage(b.ms, exp, func(c *gin.Context) {
|
||||
content.GET("/movie/:id", WhereaboutsRequired(), cache.CachePage(b.ms, exp, func(c *gin.Context) {
|
||||
if c.Param("id") == "" {
|
||||
c.Status(400)
|
||||
return
|
||||
@@ -122,7 +122,7 @@ func (b *BaseRouter) addContentRoutes() {
|
||||
}))
|
||||
|
||||
// Get tv details (for tv page)
|
||||
content.Use(WhereaboutsRequired()).GET("/tv/:id", cache.CachePage(b.ms, exp, func(c *gin.Context) {
|
||||
content.GET("/tv/:id", WhereaboutsRequired(), cache.CachePage(b.ms, exp, func(c *gin.Context) {
|
||||
if c.Param("id") == "" {
|
||||
c.Status(400)
|
||||
return
|
||||
@@ -240,6 +240,16 @@ func (b *BaseRouter) addContentRoutes() {
|
||||
}
|
||||
c.JSON(http.StatusOK, content)
|
||||
}))
|
||||
|
||||
// Available regions for watch providers
|
||||
content.GET("/regions", func(c *gin.Context) {
|
||||
r, err := regions()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (b *BaseRouter) addGameRoutes() {
|
||||
@@ -388,12 +398,8 @@ func (b *BaseRouter) addWatchedRoutes() {
|
||||
|
||||
watched.DELETE(":id", func(c *gin.Context) {
|
||||
id, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
c.Status(400)
|
||||
return
|
||||
}
|
||||
userId := c.MustGet("userId").(uint)
|
||||
if err == nil {
|
||||
userId := c.MustGet("userId").(uint)
|
||||
response, err := removeWatched(b.db, userId, uint(id))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
|
||||
|
||||
@@ -491,6 +491,14 @@ type TMDBKeywords struct {
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
type TMDBRegions struct {
|
||||
Results []struct {
|
||||
ISO3166_1 string `json:"iso_3166_1"`
|
||||
English_Name string `json:"english_name"`
|
||||
Native_Name string `json:"native_name"`
|
||||
} `json:"results"`
|
||||
}
|
||||
|
||||
func getTMDBKey() string {
|
||||
if Config.TMDB_KEY != "" {
|
||||
return Config.TMDB_KEY
|
||||
|
||||
@@ -58,12 +58,16 @@ func userUpdate(db *gorm.DB, userId uint, ur UserSettings) (UserSettings, error)
|
||||
if ur.IncludePreviouslyWatched != nil {
|
||||
user.IncludePreviouslyWatched = ur.IncludePreviouslyWatched
|
||||
}
|
||||
if ur.Country != nil {
|
||||
user.Country = ur.Country
|
||||
}
|
||||
db.Save(&user)
|
||||
return UserSettings{
|
||||
Private: user.Private,
|
||||
PrivateThoughts: user.PrivateThoughts,
|
||||
HideSpoilers: user.HideSpoilers,
|
||||
IncludePreviouslyWatched: user.IncludePreviouslyWatched,
|
||||
Country: user.Country,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -80,6 +84,7 @@ func userGetSettings(db *gorm.DB, userId uint) (UserSettings, error) {
|
||||
PrivateThoughts: user.PrivateThoughts,
|
||||
HideSpoilers: user.HideSpoilers,
|
||||
IncludePreviouslyWatched: user.IncludePreviouslyWatched,
|
||||
Country: user.Country,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
+47
-2
@@ -1,8 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { DropDownItem } from "@/types";
|
||||
import Icon from "./Icon.svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export let options: string[] | DropDownItem[];
|
||||
// If we are using DropDownItems[] as options.
|
||||
export let isDropDownItem = false;
|
||||
export let active: string | number | undefined = undefined;
|
||||
export let placeholder: string;
|
||||
export let blendIn: boolean = false;
|
||||
@@ -11,15 +14,54 @@
|
||||
|
||||
let activeValue: string;
|
||||
let open = false;
|
||||
let ulElement: HTMLUListElement;
|
||||
let mainElement: HTMLDivElement;
|
||||
|
||||
function handleKeyPress(event: KeyboardEvent) {
|
||||
if (!open || disabled) return; // Don't handle if closed or disabled
|
||||
|
||||
const pressedLetter = event.key.toLowerCase();
|
||||
|
||||
// Filter options that start with the pressed letter (ignoring case)
|
||||
const filteredOptions = options.filter((o) =>
|
||||
typeof o === "string"
|
||||
? o.toLowerCase().startsWith(pressedLetter)
|
||||
: o.value.toLowerCase().startsWith(pressedLetter)
|
||||
);
|
||||
|
||||
// If there are filtered options, select the first one
|
||||
let f: string | number | undefined;
|
||||
if (filteredOptions.length > 0) {
|
||||
f = typeof filteredOptions[0] === "string" ? filteredOptions[0] : filteredOptions[0].id;
|
||||
}
|
||||
|
||||
// Find first button with text content starting with letter pressed and scroll it into view.
|
||||
const btns = ulElement?.querySelectorAll("button");
|
||||
for (let i = 0; i < btns.length; i++) {
|
||||
const btn = btns[i];
|
||||
if (btn.textContent?.toLowerCase()?.startsWith(pressedLetter)) {
|
||||
btn.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
if (typeof active === "string") {
|
||||
if (typeof active === "string" && !isDropDownItem) {
|
||||
activeValue = active;
|
||||
} else {
|
||||
const v = options.find((o) => (typeof o !== "string" ? o.id === active : false));
|
||||
if (v && typeof v !== "string") activeValue = v.value;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mainElement.addEventListener("keypress", handleKeyPress);
|
||||
|
||||
return () => {
|
||||
mainElement.removeEventListener("keypress", handleKeyPress);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div
|
||||
@@ -28,12 +70,13 @@
|
||||
typeof active === "undefined" ? "placeholder-shown" : "",
|
||||
blendIn ? "blend-in" : ""
|
||||
].join(" ")}
|
||||
bind:this={mainElement}
|
||||
>
|
||||
<button on:click={() => (open = !open)} {disabled}>
|
||||
{activeValue ? activeValue : placeholder}
|
||||
<Icon i="chevron" facing={open ? "up" : "down"} />
|
||||
</button>
|
||||
<ul>
|
||||
<ul bind:this={ulElement}>
|
||||
{#each options.filter((o) => (typeof o === "string" ? o !== active : o.id !== active)) as o}
|
||||
<li>
|
||||
<button
|
||||
@@ -111,6 +154,8 @@
|
||||
border-bottom-right-radius: 5px;
|
||||
background-color: $bg-color;
|
||||
z-index: 99;
|
||||
max-height: 20vh;
|
||||
overflow-y: auto;
|
||||
|
||||
li {
|
||||
width: 100%;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<script lang="ts">
|
||||
import axios from "axios";
|
||||
import DropDown from "./DropDown.svelte";
|
||||
import type { DropDownItem, TMDBRegions } from "@/types";
|
||||
import Error from "./Error.svelte";
|
||||
|
||||
export let selectedCountry: string = "US";
|
||||
export let disabled = false;
|
||||
export let onChange: (country: string) => void;
|
||||
|
||||
let mappedCountries: DropDownItem[];
|
||||
|
||||
async function getCountries() {
|
||||
const c = (await axios.get(`/content/regions`)).data as TMDBRegions;
|
||||
mappedCountries = c.results
|
||||
.map((cc) => {
|
||||
return {
|
||||
id: cc.iso_3166_1,
|
||||
value: cc.english_name
|
||||
} as DropDownItem;
|
||||
})
|
||||
.sort((a, b) => a.value.localeCompare(b.value));
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await getCountries() then}
|
||||
<DropDown
|
||||
placeholder="Select a country"
|
||||
bind:active={selectedCountry}
|
||||
options={mappedCountries}
|
||||
onChange={() => onChange(selectedCountry)}
|
||||
isDropDownItem={true}
|
||||
{disabled}
|
||||
/>
|
||||
{:catch err}
|
||||
<Error error={err} pretty="Failed to load countries!" />
|
||||
{/await}
|
||||
@@ -6,7 +6,7 @@
|
||||
import Status from "@/lib/Status.svelte";
|
||||
import HorizontalList from "@/lib/HorizontalList.svelte";
|
||||
import { contentExistsOnJellyfin, updateWatched } from "@/lib/util/api";
|
||||
import { serverFeatures, watchedList } from "@/store";
|
||||
import { serverFeatures, userSettings, watchedList } from "@/store";
|
||||
import type {
|
||||
ArrDetailsResponse,
|
||||
TMDBContentCredits,
|
||||
@@ -29,6 +29,8 @@
|
||||
import FollowedThoughts from "@/lib/content/FollowedThoughts.svelte";
|
||||
import ArrRequestButton from "@/lib/request/ArrRequestButton.svelte";
|
||||
|
||||
$: settings = $userSettings;
|
||||
|
||||
export let data;
|
||||
|
||||
let trailer: string | undefined;
|
||||
@@ -66,7 +68,9 @@
|
||||
if (!movieId) {
|
||||
return;
|
||||
}
|
||||
const data = (await axios.get(`/content/movie/${movieId}`)).data as TMDBMovieDetails;
|
||||
const data = (
|
||||
await axios.get(`/content/movie/${movieId}`, { params: { region: settings?.country } })
|
||||
).data as TMDBMovieDetails;
|
||||
if (data.videos?.results?.length > 0) {
|
||||
const t = data.videos.results.find((v) => v.type?.toLowerCase() === "trailer");
|
||||
if (t?.key) {
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import UserAvatar from "@/lib/img/UserAvatar.svelte";
|
||||
import PwChangeModal from "@/routes/(app)/profile/modals/PwChangeModal.svelte";
|
||||
import SyncModal from "./modals/SyncModal.svelte";
|
||||
import RegionDropDown from "@/lib/RegionDropDown.svelte";
|
||||
|
||||
$: user = $userInfo;
|
||||
$: settings = $userSettings;
|
||||
@@ -24,6 +25,7 @@
|
||||
let privateThoughtsDisabled = false;
|
||||
let exportDisabled = false;
|
||||
let hideSpoilersDisabled = false;
|
||||
let countryDisabled = false;
|
||||
let includePreviouslyWatchedDisabled = false;
|
||||
let pwChangeModalOpen = false;
|
||||
let getProfilePromise = getProfile();
|
||||
@@ -194,6 +196,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Setting
|
||||
title="Country"
|
||||
desc="What country would you like to see available streaming providers for?"
|
||||
>
|
||||
<RegionDropDown
|
||||
selectedCountry={settings?.country}
|
||||
disabled={countryDisabled}
|
||||
onChange={(c) => {
|
||||
countryDisabled = true;
|
||||
updateUserSetting("country", c, () => {
|
||||
countryDisabled = false;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Setting>
|
||||
|
||||
<Setting title="Private" desc="Hide your profile from others?" row>
|
||||
<Checkbox
|
||||
name="private"
|
||||
@@ -261,6 +279,7 @@
|
||||
}}
|
||||
/>
|
||||
</Setting>
|
||||
|
||||
<div class="row btns">
|
||||
<button on:click={() => goto("/import")}>Import</button>
|
||||
<button on:click={() => downloadWatchedList()} disabled={exportDisabled}>Export</button>
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
<script lang="ts">
|
||||
import Checkbox from "@/lib/Checkbox.svelte";
|
||||
import DropDown from "@/lib/DropDown.svelte";
|
||||
import PageError from "@/lib/PageError.svelte";
|
||||
import Spinner from "@/lib/Spinner.svelte";
|
||||
import { notify } from "@/lib/util/notify";
|
||||
import type { Content, RadarrSettings, ServerConfig, SonarrSettings } from "@/types";
|
||||
import type {
|
||||
Content,
|
||||
RadarrSettings,
|
||||
ServerConfig,
|
||||
SonarrSettings,
|
||||
DropDownItem
|
||||
} from "@/types";
|
||||
import axios from "axios";
|
||||
import SonarrModal from "./modals/SonarrModal.svelte";
|
||||
import SettingsList from "@/lib/settings/SettingsList.svelte";
|
||||
@@ -15,6 +22,7 @@
|
||||
import Error from "@/lib/Error.svelte";
|
||||
import Stat from "@/lib/stats/Stat.svelte";
|
||||
import TwitchModal from "./modals/TwitchModal.svelte";
|
||||
import RegionDropDown from "@/lib/RegionDropDown.svelte";
|
||||
|
||||
let serverConfig: ServerConfig;
|
||||
let sonarrModalOpen = false;
|
||||
@@ -30,6 +38,10 @@
|
||||
let jfDisabled = false;
|
||||
let tmdbkDisabled = false;
|
||||
let plexHostDisabled = false;
|
||||
let countryDisabled = false;
|
||||
let selectedCountry: string;
|
||||
let countries: any;
|
||||
let countriesDropdown: DropDownItem[] = [];
|
||||
|
||||
async function getServerConfig() {
|
||||
serverConfig = (await axios.get(`/server/config`)).data as ServerConfig;
|
||||
@@ -120,6 +132,21 @@
|
||||
<Spinner />
|
||||
{:then}
|
||||
<h3>General</h3>
|
||||
<Setting
|
||||
title="Default Country"
|
||||
desc="Default country for new users. This can be changed per user and won't affect existing users."
|
||||
>
|
||||
<RegionDropDown
|
||||
selectedCountry={serverConfig.DEFAULT_COUNTRY}
|
||||
disabled={countryDisabled}
|
||||
onChange={(c) => {
|
||||
countryDisabled = true;
|
||||
updateServerConfig("DEFAULT_COUNTRY", c, () => {
|
||||
countryDisabled = false;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Setting>
|
||||
<Setting
|
||||
title="Jellyfin Host"
|
||||
desc="Point to your Jellyfin server to enable related features. Don't change server after
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
import VideoEmbedModal from "@/lib/content/VideoEmbedModal.svelte";
|
||||
import { contentExistsOnJellyfin, updateWatched } from "@/lib/util/api";
|
||||
import { getTopCrew } from "@/lib/util/helpers.js";
|
||||
import { serverFeatures, watchedList } from "@/store";
|
||||
import { serverFeatures, userSettings, watchedList } from "@/store";
|
||||
import type {
|
||||
TMDBContentCredits,
|
||||
TMDBContentCreditsCrew,
|
||||
@@ -29,6 +29,8 @@
|
||||
import FollowedThoughts from "@/lib/content/FollowedThoughts.svelte";
|
||||
import ArrRequestButton from "@/lib/request/ArrRequestButton.svelte";
|
||||
|
||||
$: settings = $userSettings;
|
||||
|
||||
export let data;
|
||||
|
||||
let trailer: string | undefined;
|
||||
@@ -64,7 +66,9 @@
|
||||
if (!showId) {
|
||||
return;
|
||||
}
|
||||
const data = (await axios.get(`/content/tv/${showId}`)).data as TMDBShowDetails;
|
||||
const data = (
|
||||
await axios.get(`/content/tv/${showId}`, { params: { region: settings?.country } })
|
||||
).data as TMDBShowDetails;
|
||||
if (data.videos?.results?.length > 0) {
|
||||
const t = data.videos.results.find((v) => v.type?.toLowerCase() === "trailer");
|
||||
if (t?.key) {
|
||||
|
||||
+11
-1
@@ -151,6 +151,7 @@ export interface UserSettings {
|
||||
privateThoughts: boolean;
|
||||
hideSpoilers: boolean;
|
||||
includePreviouslyWatched: boolean;
|
||||
country: string;
|
||||
}
|
||||
|
||||
export interface ChangePasswordForm {
|
||||
@@ -659,6 +660,14 @@ export interface TMDBKeywords {
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface TMDBRegions {
|
||||
results: {
|
||||
iso_3166_1: string;
|
||||
english_name: string;
|
||||
native_name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface ContentSearch {
|
||||
page: number;
|
||||
results: (ContentSearchMovie | ContentSearchTv | ContentSearchPerson)[];
|
||||
@@ -769,6 +778,7 @@ export interface ManagedUser {
|
||||
}
|
||||
|
||||
export interface ServerConfig {
|
||||
DEFAULT_COUNTRY: string;
|
||||
JELLYFIN_HOST: string;
|
||||
SIGNUP_ENABLED: boolean;
|
||||
TMDB_KEY: string;
|
||||
@@ -805,7 +815,7 @@ export interface TwitchSettings {
|
||||
}
|
||||
|
||||
export interface DropDownItem {
|
||||
id: number;
|
||||
id: number | string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user