Fix lots of bugs (#354)

This commit is contained in:
Zoe Roux 2024-03-25 00:43:47 +01:00 committed by GitHub
commit 66fa07f341
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1662 additions and 246 deletions

64
.gitattributes vendored
View File

@ -1,63 +1 @@
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto
###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs diff=csharp
###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln merge=binary
#*.csproj merge=binary
#*.vbproj merge=binary
#*.vcxproj merge=binary
#*.vcproj merge=binary
#*.dbproj merge=binary
#*.fsproj merge=binary
#*.lsproj merge=binary
#*.wixproj merge=binary
#*.modelproj merge=binary
#*.sqlproj merge=binary
#*.wwaproj merge=binary
###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg binary
#*.png binary
#*.gif binary
###############################################################################
# diff behavior for common document formats
#
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the
# entries below.
###############################################################################
#*.doc diff=astextplain
#*.DOC diff=astextplain
#*.docx diff=astextplain
#*.DOCX diff=astextplain
#*.dot diff=astextplain
#*.DOT diff=astextplain
#*.pdf diff=astextplain
#*.PDF diff=astextplain
#*.rtf diff=astextplain
#*.RTF diff=astextplain
*.Designer.cs linguist-generated=true

View File

@ -24,6 +24,9 @@ jobs:
- context: ./scanner
label: scanner
image: zoriya/kyoo_scanner
- context: ./autosync
label: autosync
image: zoriya/kyoo_autosync
- context: ./transcoder
label: transcoder
image: zoriya/kyoo_transcoder

View File

@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="Autofac" Version="8.0.0" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Dapper" Version="2.1.37" />
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />

View File

@ -151,7 +151,7 @@ public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, IN
/// <summary>
/// The release date of this episode. It can be null if unknown.
/// </summary>
public DateTime? ReleaseDate { get; set; }
public DateOnly? ReleaseDate { get; set; }
/// <inheritdoc />
public DateTime AddedDate { get; set; }

View File

@ -20,6 +20,7 @@ using System;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Kyoo.Abstractions.Models.Attributes;
@ -47,7 +48,7 @@ public interface IThumbnails
public Image? Logo { get; set; }
}
[TypeConverter(typeof(ImageConvertor))]
[JsonConverter(typeof(ImageConvertor))]
[SqlFirstColumn(nameof(Source))]
public class Image
{
@ -71,32 +72,32 @@ public class Image
Blurhash = blurhash ?? "000000";
}
public class ImageConvertor : TypeConverter
public class ImageConvertor : JsonConverter<Image>
{
/// <inheritdoc />
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
{
if (sourceType == typeof(string))
return true;
return base.CanConvertFrom(context, sourceType);
}
/// <inheritdoc />
public override object ConvertFrom(
ITypeDescriptorContext? context,
CultureInfo? culture,
object value
public override Image? Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options
)
{
if (value is not string source)
return base.ConvertFrom(context, culture, value)!;
return new Image(source);
if (reader.TokenType == JsonTokenType.String && reader.GetString() is string source)
return new Image(source);
using JsonDocument document = JsonDocument.ParseValue(ref reader);
return document.RootElement.Deserialize<Image>();
}
/// <inheritdoc />
public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType)
public override void Write(
Utf8JsonWriter writer,
Image value,
JsonSerializerOptions options
)
{
return false;
writer.WriteStartObject();
writer.WriteString("source", value.Source);
writer.WriteString("blurhash", value.Blurhash);
writer.WriteEndObject();
}
}
}

View File

@ -104,7 +104,7 @@ public class Movie
/// <summary>
/// The date this movie aired.
/// </summary>
public DateTime? AirDate { get; set; }
public DateOnly? AirDate { get; set; }
/// <inheritdoc />
public DateTime AddedDate { get; set; }
@ -120,11 +120,11 @@ public class Movie
[JsonIgnore]
[Column("air_date")]
public DateTime? StartAir => AirDate;
public DateOnly? StartAir => AirDate;
[JsonIgnore]
[Column("air_date")]
public DateTime? EndAir => AirDate;
public DateOnly? EndAir => AirDate;
/// <summary>
/// A video of a few minutes that tease the content.

View File

@ -97,7 +97,7 @@ public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
/// <summary>
/// The starting air date of this season.
/// </summary>
public DateTime? StartDate { get; set; }
public DateOnly? StartDate { get; set; }
/// <inheritdoc />
public DateTime AddedDate { get; set; }
@ -105,7 +105,7 @@ public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
/// <summary>
/// The ending date of this season.
/// </summary>
public DateTime? EndDate { get; set; }
public DateOnly? EndDate { get; set; }
/// <inheritdoc />
public Image? Poster { get; set; }

View File

@ -94,13 +94,13 @@ public class Show
/// <summary>
/// The date this show started airing. It can be null if this is unknown.
/// </summary>
public DateTime? StartAir { get; set; }
public DateOnly? StartAir { get; set; }
/// <summary>
/// The date this show finished airing.
/// It can also be null if this is unknown.
/// </summary>
public DateTime? EndAir { get; set; }
public DateOnly? EndAir { get; set; }
/// <inheritdoc />
public DateTime AddedDate { get; set; }
@ -121,7 +121,7 @@ public class Show
[JsonIgnore]
[Column("start_air")]
public DateTime? AirDate => StartAir;
public DateOnly? AirDate => StartAir;
/// <inheritdoc />
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();

View File

@ -8,7 +8,7 @@
<ItemGroup>
<PackageReference Include="AspNetCore.Proxy" Version="4.5.0" />
<PackageReference Include="Blurhash.SkiaSharp" Version="2.0.0" />
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Dapper" Version="2.1.37" />
<PackageReference Include="InterpolatedSql.Dapper" Version="2.3.0" />
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.0.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />

View File

@ -6,7 +6,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Dapper" Version="2.1.37" />
<PackageReference Include="EFCore.NamingConventions" Version="8.0.3" />
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
<PackageReference Include="InterpolatedSql.Dapper" Version="2.3.0" />

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,181 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Kyoo.Postgresql.Migrations
{
/// <inheritdoc />
public partial class UseDateOnly : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder
.AlterDatabase()
.Annotation(
"Npgsql:Enum:genre",
"action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western"
)
.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.Annotation("Npgsql:Enum:watch_status", "completed,watching,droped,planned,deleted")
.OldAnnotation(
"Npgsql:Enum:genre",
"action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western"
)
.OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.OldAnnotation("Npgsql:Enum:watch_status", "completed,watching,droped,planned");
migrationBuilder.AlterColumn<DateOnly>(
name: "start_air",
table: "shows",
type: "date",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true
);
migrationBuilder.AlterColumn<DateOnly>(
name: "end_air",
table: "shows",
type: "date",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true
);
migrationBuilder.AlterColumn<DateOnly>(
name: "start_date",
table: "seasons",
type: "date",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true
);
migrationBuilder.AlterColumn<DateOnly>(
name: "end_date",
table: "seasons",
type: "date",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true
);
migrationBuilder.AlterColumn<DateOnly>(
name: "air_date",
table: "movies",
type: "date",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true
);
migrationBuilder.AlterColumn<DateOnly>(
name: "release_date",
table: "episodes",
type: "date",
nullable: true,
oldClrType: typeof(DateTime),
oldType: "timestamp with time zone",
oldNullable: true
);
migrationBuilder.CreateIndex(
name: "ix_users_username",
table: "users",
column: "username",
unique: true
);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropIndex(name: "ix_users_username", table: "users");
migrationBuilder
.AlterDatabase()
.Annotation(
"Npgsql:Enum:genre",
"action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western"
)
.Annotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.Annotation("Npgsql:Enum:watch_status", "completed,watching,droped,planned")
.OldAnnotation(
"Npgsql:Enum:genre",
"action,adventure,animation,comedy,crime,documentary,drama,family,fantasy,history,horror,music,mystery,romance,science_fiction,thriller,war,western"
)
.OldAnnotation("Npgsql:Enum:status", "unknown,finished,airing,planned")
.OldAnnotation(
"Npgsql:Enum:watch_status",
"completed,watching,droped,planned,deleted"
);
migrationBuilder.AlterColumn<DateTime>(
name: "start_air",
table: "shows",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateOnly),
oldType: "date",
oldNullable: true
);
migrationBuilder.AlterColumn<DateTime>(
name: "end_air",
table: "shows",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateOnly),
oldType: "date",
oldNullable: true
);
migrationBuilder.AlterColumn<DateTime>(
name: "start_date",
table: "seasons",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateOnly),
oldType: "date",
oldNullable: true
);
migrationBuilder.AlterColumn<DateTime>(
name: "end_date",
table: "seasons",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateOnly),
oldType: "date",
oldNullable: true
);
migrationBuilder.AlterColumn<DateTime>(
name: "air_date",
table: "movies",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateOnly),
oldType: "date",
oldNullable: true
);
migrationBuilder.AlterColumn<DateTime>(
name: "release_date",
table: "episodes",
type: "timestamp with time zone",
nullable: true,
oldClrType: typeof(DateOnly),
oldType: "date",
oldNullable: true
);
}
}
}

View File

@ -19,12 +19,12 @@ namespace Kyoo.Postgresql.Migrations
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "7.0.12")
.HasAnnotation("ProductVersion", "8.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "genre", new[] { "action", "adventure", "animation", "comedy", "crime", "documentary", "drama", "family", "fantasy", "history", "horror", "music", "mystery", "romance", "science_fiction", "thriller", "war", "western" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "status", new[] { "unknown", "finished", "airing", "planned" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "watch_status", new[] { "completed", "watching", "droped", "planned" });
NpgsqlModelBuilderExtensions.HasPostgresEnum(modelBuilder, "watch_status", new[] { "completed", "watching", "droped", "planned", "deleted" });
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b =>
@ -109,8 +109,8 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("text")
.HasColumnName("path");
b.Property<DateTime?>("ReleaseDate")
.HasColumnType("timestamp with time zone")
b.Property<DateOnly?>("ReleaseDate")
.HasColumnType("date")
.HasColumnName("release_date");
b.Property<int?>("Runtime")
@ -238,8 +238,8 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnName("added_date")
.HasDefaultValueSql("now() at time zone 'utc'");
b.Property<DateTime?>("AirDate")
.HasColumnType("timestamp with time zone")
b.Property<DateOnly?>("AirDate")
.HasColumnType("date")
.HasColumnName("air_date");
b.Property<string[]>("Aliases")
@ -373,8 +373,8 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnName("added_date")
.HasDefaultValueSql("now() at time zone 'utc'");
b.Property<DateTime?>("EndDate")
.HasColumnType("timestamp with time zone")
b.Property<DateOnly?>("EndDate")
.HasColumnType("date")
.HasColumnName("end_date");
b.Property<string>("ExternalId")
@ -404,8 +404,8 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("character varying(256)")
.HasColumnName("slug");
b.Property<DateTime?>("StartDate")
.HasColumnType("timestamp with time zone")
b.Property<DateOnly?>("StartDate")
.HasColumnType("date")
.HasColumnName("start_date");
b.HasKey("Id")
@ -440,8 +440,8 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("text[]")
.HasColumnName("aliases");
b.Property<DateTime?>("EndAir")
.HasColumnType("timestamp with time zone")
b.Property<DateOnly?>("EndAir")
.HasColumnType("date")
.HasColumnName("end_air");
b.Property<string>("ExternalId")
@ -473,8 +473,8 @@ namespace Kyoo.Postgresql.Migrations
.HasColumnType("character varying(256)")
.HasColumnName("slug");
b.Property<DateTime?>("StartAir")
.HasColumnType("timestamp with time zone")
b.Property<DateOnly?>("StartAir")
.HasColumnType("date")
.HasColumnName("start_air");
b.Property<Status>("Status")
@ -651,6 +651,10 @@ namespace Kyoo.Postgresql.Migrations
.IsUnique()
.HasDatabaseName("ix_users_slug");
b.HasIndex("Username")
.IsUnique()
.HasDatabaseName("ix_users_username");
b.ToTable("users", (string)null);
});

View File

@ -55,7 +55,7 @@ import arrayShuffle from "array-shuffle";
import { Tooltip } from "react-tooltip";
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
import { PortalProvider } from "@gorhom/portal";
import { ConnectionError, ErrorContext } from "@kyoo/ui";
import { ConnectionError } from "@kyoo/ui";
const font = Poppins({ weight: ["300", "400", "900"], subsets: ["latin"], display: "swap" });
@ -136,17 +136,7 @@ const WithLayout = ({ Component, ...props }: { Component: ComponentType }) => {
const layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
const { Layout, props: layoutProps } =
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
return (
<Layout
page={
<ErrorContext>
<Component {...props} />
</ErrorContext>
}
randomItems={[]}
{...layoutProps}
/>
);
return <Layout page={<Component {...props} />} randomItems={[]} {...layoutProps} />;
};
const App = ({ Component, pageProps }: AppProps) => {

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { ReactNode, createContext, useContext, useEffect, useMemo, useRef } from "react";
import { ReactNode, createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
import { ServerInfoP, User, UserP } from "./resources";
import { z } from "zod";
import { zdate } from "./utils";
@ -69,7 +69,8 @@ export const ConnectionErrorContext = createContext<{
error: KyooErrors | null;
loading: boolean;
retry?: () => void;
}>({ error: null, loading: true });
setError: (error: KyooErrors) => void;
}>({ error: null, loading: true, setError: () => {} });
/* eslint-disable react-hooks/rules-of-hooks */
export const AccountProvider = ({
@ -96,6 +97,7 @@ export const AccountProvider = ({
retry: () => {
queryClient.resetQueries({ queryKey: ["auth", "me"] });
},
setError: () => {},
}}
>
{children}
@ -156,15 +158,18 @@ export const AccountProvider = ({
}
}, [selected, queryClient]);
const [permissionError, setPermissionError] = useState<KyooErrors | null>(null);
return (
<AccountContext.Provider value={accounts}>
<ConnectionErrorContext.Provider
value={{
error: selected ? initialSsrError.current ?? user.error : null,
error: selected ? initialSsrError.current ?? user.error ?? permissionError : null,
loading: user.isLoading,
retry: () => {
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
},
setError: setPermissionError,
}}
>
{children}

View File

@ -49,7 +49,7 @@ export const A = ({
replace
? {
nativeBehavior: "stack-replace",
isNestedNavigator: false,
isNestedNavigator: true,
}
: undefined
}
@ -105,7 +105,7 @@ export const Link = ({
const linkProps = useLink({
href: href ?? "#",
replace,
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
// @ts-ignore Missing hrefAttrs type definition.
linkProps.hrefAttrs = { ...linkProps.hrefAttrs, target };

View File

@ -335,7 +335,7 @@ export const EpisodeLine = ({
{isLoading || <P numberOfLines={descriptionExpanded ? undefined : 3}>{overview}</P>}
</Skeleton>
<IconButton
{...css(["more"])}
{...css(["more", Platform.OS !== "web" && { opacity: 1 }])}
icon={descriptionExpanded ? ExpandLess : ExpandMore}
{...tooltip(t(descriptionExpanded ? "misc.collapse" : "misc.expand"))}
onPress={(e) => {

View File

@ -18,21 +18,49 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { ConnectionErrorContext } from "@kyoo/models";
import { Button, H1, P, ts } from "@kyoo/primitives";
import { ConnectionErrorContext, useAccount } from "@kyoo/models";
import { Button, H1, Icon, Link, P, ts } from "@kyoo/primitives";
import { useRouter } from "solito/router";
import { useContext } from "react";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { DefaultLayout } from "../layout";
import { ErrorView } from "./error";
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
export const ConnectionError = () => {
const { css } = useYoshiki();
const { t } = useTranslation();
const router = useRouter();
const { error, retry } = useContext(ConnectionErrorContext);
const account = useAccount();
if (error && (error.status === 401 || error.status == 403)) {
if (!account) {
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
>
<P>{t("errors.needAccount")}</P>
<Button
as={Link}
href={"/register"}
text={t("login.register")}
licon={<Icon icon={Register} {...css({ marginRight: ts(2) })} />}
/>
</View>
);
}
if (account.isVerified) return <ErrorView error={error} noBubble />;
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
>
<P>{t("errors.needVerification")}</P>
</View>
);
}
return (
<View {...css({ padding: ts(2) })}>
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>

View File

@ -18,19 +18,11 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { KyooErrors, useAccount } from "@kyoo/models";
import { ConnectionErrorContext, KyooErrors } from "@kyoo/models";
import { P } from "@kyoo/primitives";
import {
ReactElement,
createContext,
useContext,
useEffect,
useLayoutEffect,
useState,
} from "react";
import { useContext, useLayoutEffect } from "react";
import { View } from "react-native";
import { useYoshiki } from "yoshiki/native";
import { PermissionError } from "./unauthorized";
export const ErrorView = ({
error,
@ -40,7 +32,7 @@ export const ErrorView = ({
noBubble?: boolean;
}) => {
const { css } = useYoshiki();
const setError = useErrorContext();
const { setError } = useContext(ConnectionErrorContext);
useLayoutEffect(() => {
// if this is a permission error, make it go up the tree to have a whole page login screen.
@ -65,22 +57,3 @@ export const ErrorView = ({
</View>
);
};
const ErrorCtx = createContext<(val: KyooErrors | null) => void>(null!);
export const ErrorContext = ({ children }: { children: ReactElement }) => {
const [error, setError] = useState<KyooErrors | null>(null);
const account = useAccount();
useEffect(() => {
setError(null);
}, [account, children]);
if (error && (error.status === 401 || error.status === 403))
return <PermissionError error={error} />;
if (error) return <ErrorView error={error} noBubble />;
return <ErrorCtx.Provider value={setError}>{children}</ErrorCtx.Provider>;
};
export const useErrorContext = () => {
return useContext(ErrorCtx);
};

View File

@ -18,13 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/
import { KyooErrors, useAccount } from "@kyoo/models";
import { Button, Icon, Link, P, ts } from "@kyoo/primitives";
import { P } from "@kyoo/primitives";
import { useTranslation } from "react-i18next";
import { View } from "react-native";
import { rem, useYoshiki } from "yoshiki/native";
import { ErrorView } from "./error";
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
import { useYoshiki } from "yoshiki/native";
export const Unauthorized = ({ missing }: { missing: string[] }) => {
const { t } = useTranslation();
@ -43,31 +40,3 @@ export const Unauthorized = ({ missing }: { missing: string[] }) => {
</View>
);
};
export const PermissionError = ({ error }: { error: KyooErrors }) => {
const { t } = useTranslation();
const { css } = useYoshiki();
const account = useAccount();
if (!account) {
return (
<View
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
>
<P>{t("errors.needAccount")}</P>
<Button
as={Link}
href={"/register"}
text={t("login.register")}
licon={<Icon icon={Register} {...css({ marginRight: ts(2) })} />}
/>
</View>
);
}
if (account.isVerified) return <ErrorView error={error} noBubble />;
return (
<View {...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}>
<P>{t("errors.needVerification")}</P>
</View>
);
};

View File

@ -46,7 +46,7 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
useEffect(() => {
if (!apiUrl && Platform.OS !== "web")
router.replace("/server-url", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
}, [apiUrl, router]);
@ -74,7 +74,7 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
setError(error);
if (error) return;
router.replace("/", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
}}
{...css({

View File

@ -106,7 +106,7 @@ export const OidcCallbackPage: QueryPage<{
function onError(error: string) {
router.replace({ pathname: "/login", query: { error, apiUrl } }, undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
}
async function run() {
@ -114,7 +114,7 @@ export const OidcCallbackPage: QueryPage<{
if (loginError) onError(loginError);
else {
router.replace("/", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
}
}

View File

@ -45,7 +45,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
useEffect(() => {
if (!apiUrl && Platform.OS !== "web")
router.replace("/server-url", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
}, [apiUrl, router]);
@ -85,7 +85,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
setError(error);
if (error) return;
router.replace("/", undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
}}
{...css({

View File

@ -157,11 +157,11 @@ export const Player = ({
if (!data) return;
if (data.type === "movie")
router.replace(`/movie/${data.slug}`, undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
else
router.replace(next ?? `/show/${data.show!.slug}`, undefined, {
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
});
}}
{...css(StyleSheet.absoluteFillObject)}

View File

@ -220,7 +220,7 @@ export const Video = memo(function Video({
onLoad={(info) => {
setDuration(info.duration);
}}
onPlayPause={setPlay}
onPlaybackStateChanged={(state) => setPlay(state.isPlaying)}
fonts={fonts}
subtitles={subtitles}
onMediaUnsupported={() => {

View File

@ -24,7 +24,6 @@ declare module "react-native-video" {
interface ReactVideoProps {
fonts?: string[];
subtitles?: Subtitle[];
onPlayPause: (isPlaying: boolean) => void;
onMediaUnsupported?: () => void;
}
export type VideoProps = Omit<ReactVideoProps, "source"> & {
@ -103,13 +102,18 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
onLoad?.(info);
}}
onBuffer={onBuffer}
onError={onMediaUnsupported}
onError={(error) => {
console.error(error);
if (mode === PlayMode.Direct) onMediaUnsupported?.();
else onError?.(error);
}}
selectedVideoTrack={
video === -1
? { type: SelectedVideoTrackType.AUDO }
: { type: SelectedVideoTrackType.RESOLUTION, value: video }
}
selectedAudioTrack={{ type: SelectedTrackType.INDEX, value: audio.index }}
// when video file is invalid, audio is undefined
selectedAudioTrack={{ type: SelectedTrackType.INDEX, value: audio?.index ?? 0 }}
textTracks={subtitles?.map((x) => ({
type: MimeTypes.get(x.codec) as any,
uri: x.link!,

View File

@ -115,7 +115,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
onProgress,
onError,
onEnd,
onPlayPause,
onPlaybackStateChanged,
onMediaUnsupported,
fonts,
},
@ -182,7 +182,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
if (!hls) return;
const update = () => {
if (!hls) return;
hls.audioTrack = audio.index;
hls.audioTrack = audio?.index ?? 0;
};
update();
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, update);
@ -234,9 +234,8 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
onLoadedMetadata={() => {
if (source.startPosition) setProgress(source.startPosition / 1000);
}}
// BUG: If this is enabled, switching to fullscreen or opening a menu make a play/pause loop until firefox crash.
// onPlay={() => onPlayPause?.call(null, true)}
// onPause={() => onPlayPause?.call(null, false)}
onPlay={() => onPlaybackStateChanged?.({ isPlaying: true })}
onPause={() => onPlaybackStateChanged?.({ isPlaying: false })}
onEnded={onEnd}
{...css({ width: "100%", height: "100%", objectFit: "contain" })}
/>

View File

@ -9,13 +9,12 @@ import (
"path/filepath"
"strconv"
"strings"
"sync"
"github.com/zoriya/go-mediainfo"
)
type MediaInfo struct {
// closed if the mediainfo is ready for read. open otherwise
ready <-chan struct{}
// The sha1 of the video file.
Sha string `json:"sha"`
/// The internal path of the video file.
@ -177,37 +176,38 @@ var SubtitleExtensions = map[string]string{
"vtt": "vtt",
}
var infos = NewCMap[string, *MediaInfo]()
type MICache struct {
info *MediaInfo
ready sync.WaitGroup
}
var infos = NewCMap[string, *MICache]()
func GetInfo(path string, sha string, route string) (*MediaInfo, error) {
var err error
ret, _ := infos.GetOrCreate(sha, func() *MediaInfo {
readyChan := make(chan struct{})
mi := &MediaInfo{
Sha: sha,
ready: readyChan,
}
ret, _ := infos.GetOrCreate(sha, func() *MICache {
mi := &MICache{info: &MediaInfo{Sha: sha}}
mi.ready.Add(1)
go func() {
save_path := fmt.Sprintf("%s/%s/info.json", Settings.Metadata, sha)
if err := getSavedInfo(save_path, mi); err == nil {
if err := getSavedInfo(save_path, mi.info); err == nil {
log.Printf("Using mediainfo cache on filesystem for %s", path)
close(readyChan)
mi.ready.Done()
return
}
var val *MediaInfo
val, err = getInfo(path, route)
*mi = *val
mi.ready = readyChan
mi.Sha = sha
close(readyChan)
saveInfo(save_path, mi)
*mi.info = *val
mi.info.Sha = sha
mi.ready.Done()
saveInfo(save_path, mi.info)
}()
return mi
})
<-ret.ready
return ret, err
ret.ready.Wait()
return ret.info, err
}
func getSavedInfo[T any](save_path string, mi *T) error {

View File

@ -15,14 +15,17 @@ type Keyframe struct {
Keyframes []float64
CanTransmux bool
IsDone bool
mutex sync.RWMutex
ready sync.WaitGroup
listeners []func(keyframes []float64)
info *KeyframeInfo
}
type KeyframeInfo struct {
mutex sync.RWMutex
ready sync.WaitGroup
listeners []func(keyframes []float64)
}
func (kf *Keyframe) Get(idx int32) float64 {
kf.mutex.RLock()
defer kf.mutex.RUnlock()
kf.info.mutex.RLock()
defer kf.info.mutex.RUnlock()
return kf.Keyframes[idx]
}
@ -30,8 +33,8 @@ func (kf *Keyframe) Slice(start int32, end int32) []float64 {
if end <= start {
return []float64{}
}
kf.mutex.RLock()
defer kf.mutex.RUnlock()
kf.info.mutex.RLock()
defer kf.info.mutex.RUnlock()
ref := kf.Keyframes[start:end]
ret := make([]float64, end-start)
copy(ret, ref)
@ -39,24 +42,24 @@ func (kf *Keyframe) Slice(start int32, end int32) []float64 {
}
func (kf *Keyframe) Length() (int32, bool) {
kf.mutex.RLock()
defer kf.mutex.RUnlock()
kf.info.mutex.RLock()
defer kf.info.mutex.RUnlock()
return int32(len(kf.Keyframes)), kf.IsDone
}
func (kf *Keyframe) add(values []float64) {
kf.mutex.Lock()
defer kf.mutex.Unlock()
kf.info.mutex.Lock()
defer kf.info.mutex.Unlock()
kf.Keyframes = append(kf.Keyframes, values...)
for _, listener := range kf.listeners {
for _, listener := range kf.info.listeners {
listener(kf.Keyframes)
}
}
func (kf *Keyframe) AddListener(callback func(keyframes []float64)) {
kf.mutex.Lock()
defer kf.mutex.Unlock()
kf.listeners = append(kf.listeners, callback)
kf.info.mutex.Lock()
defer kf.info.mutex.Unlock()
kf.info.listeners = append(kf.info.listeners, callback)
}
var keyframes = NewCMap[string, *Keyframe]()
@ -66,13 +69,14 @@ func GetKeyframes(sha string, path string) *Keyframe {
kf := &Keyframe{
Sha: sha,
IsDone: false,
info: &KeyframeInfo{},
}
kf.ready.Add(1)
kf.info.ready.Add(1)
go func() {
save_path := fmt.Sprintf("%s/%s/keyframes.json", Settings.Metadata, sha)
if err := getSavedInfo(save_path, kf); err == nil {
log.Printf("Using keyframes cache on filesystem for %s", path)
kf.ready.Done()
kf.info.ready.Done()
return
}
@ -83,7 +87,7 @@ func GetKeyframes(sha string, path string) *Keyframe {
}()
return kf
})
ret.ready.Wait()
ret.info.ready.Wait()
return ret
}
@ -154,7 +158,7 @@ func getKeyframes(path string, kf *Keyframe) error {
if len(ret) == max {
kf.add(ret)
if done == 0 {
kf.ready.Done()
kf.info.ready.Done()
} else if done >= 500 {
max = 500
}
@ -165,7 +169,7 @@ func getKeyframes(path string, kf *Keyframe) error {
}
kf.add(ret)
if done == 0 {
kf.ready.Done()
kf.info.ready.Done()
}
kf.IsDone = true
return nil