mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-07-09 03:04:20 -04:00
Fix lots of bugs (#354)
This commit is contained in:
commit
66fa07f341
64
.gitattributes
vendored
64
.gitattributes
vendored
@ -1,63 +1 @@
|
|||||||
###############################################################################
|
*.Designer.cs linguist-generated=true
|
||||||
# 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
|
|
||||||
|
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
@ -24,6 +24,9 @@ jobs:
|
|||||||
- context: ./scanner
|
- context: ./scanner
|
||||||
label: scanner
|
label: scanner
|
||||||
image: zoriya/kyoo_scanner
|
image: zoriya/kyoo_scanner
|
||||||
|
- context: ./autosync
|
||||||
|
label: autosync
|
||||||
|
image: zoriya/kyoo_autosync
|
||||||
- context: ./transcoder
|
- context: ./transcoder
|
||||||
label: transcoder
|
label: transcoder
|
||||||
image: zoriya/kyoo_transcoder
|
image: zoriya/kyoo_transcoder
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Autofac" Version="8.0.0" />
|
<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="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||||
|
@ -151,7 +151,7 @@ public class Episode : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, IN
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The release date of this episode. It can be null if unknown.
|
/// The release date of this episode. It can be null if unknown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? ReleaseDate { get; set; }
|
public DateOnly? ReleaseDate { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DateTime AddedDate { get; set; }
|
public DateTime AddedDate { get; set; }
|
||||||
|
@ -20,6 +20,7 @@ using System;
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text.Json;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using Kyoo.Abstractions.Models.Attributes;
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ public interface IThumbnails
|
|||||||
public Image? Logo { get; set; }
|
public Image? Logo { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
[TypeConverter(typeof(ImageConvertor))]
|
[JsonConverter(typeof(ImageConvertor))]
|
||||||
[SqlFirstColumn(nameof(Source))]
|
[SqlFirstColumn(nameof(Source))]
|
||||||
public class Image
|
public class Image
|
||||||
{
|
{
|
||||||
@ -71,32 +72,32 @@ public class Image
|
|||||||
Blurhash = blurhash ?? "000000";
|
Blurhash = blurhash ?? "000000";
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ImageConvertor : TypeConverter
|
public class ImageConvertor : JsonConverter<Image>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
public override Image? Read(
|
||||||
{
|
ref Utf8JsonReader reader,
|
||||||
if (sourceType == typeof(string))
|
Type typeToConvert,
|
||||||
return true;
|
JsonSerializerOptions options
|
||||||
return base.CanConvertFrom(context, sourceType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override object ConvertFrom(
|
|
||||||
ITypeDescriptorContext? context,
|
|
||||||
CultureInfo? culture,
|
|
||||||
object value
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
if (value is not string source)
|
if (reader.TokenType == JsonTokenType.String && reader.GetString() is string source)
|
||||||
return base.ConvertFrom(context, culture, value)!;
|
return new Image(source);
|
||||||
return new Image(source);
|
using JsonDocument document = JsonDocument.ParseValue(ref reader);
|
||||||
|
return document.RootElement.Deserialize<Image>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ public class Movie
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The date this movie aired.
|
/// The date this movie aired.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? AirDate { get; set; }
|
public DateOnly? AirDate { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DateTime AddedDate { get; set; }
|
public DateTime AddedDate { get; set; }
|
||||||
@ -120,11 +120,11 @@ public class Movie
|
|||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[Column("air_date")]
|
[Column("air_date")]
|
||||||
public DateTime? StartAir => AirDate;
|
public DateOnly? StartAir => AirDate;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[Column("air_date")]
|
[Column("air_date")]
|
||||||
public DateTime? EndAir => AirDate;
|
public DateOnly? EndAir => AirDate;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// A video of a few minutes that tease the content.
|
/// A video of a few minutes that tease the content.
|
||||||
|
@ -97,7 +97,7 @@ public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The starting air date of this season.
|
/// The starting air date of this season.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? StartDate { get; set; }
|
public DateOnly? StartDate { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DateTime AddedDate { get; set; }
|
public DateTime AddedDate { get; set; }
|
||||||
@ -105,7 +105,7 @@ public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The ending date of this season.
|
/// The ending date of this season.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? EndDate { get; set; }
|
public DateOnly? EndDate { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Image? Poster { get; set; }
|
public Image? Poster { get; set; }
|
||||||
|
@ -94,13 +94,13 @@ public class Show
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The date this show started airing. It can be null if this is unknown.
|
/// The date this show started airing. It can be null if this is unknown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? StartAir { get; set; }
|
public DateOnly? StartAir { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The date this show finished airing.
|
/// The date this show finished airing.
|
||||||
/// It can also be null if this is unknown.
|
/// It can also be null if this is unknown.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? EndAir { get; set; }
|
public DateOnly? EndAir { get; set; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public DateTime AddedDate { get; set; }
|
public DateTime AddedDate { get; set; }
|
||||||
@ -121,7 +121,7 @@ public class Show
|
|||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
[Column("start_air")]
|
[Column("start_air")]
|
||||||
public DateTime? AirDate => StartAir;
|
public DateOnly? AirDate => StartAir;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCore.Proxy" Version="4.5.0" />
|
<PackageReference Include="AspNetCore.Proxy" Version="4.5.0" />
|
||||||
<PackageReference Include="Blurhash.SkiaSharp" Version="2.0.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="InterpolatedSql.Dapper" Version="2.3.0" />
|
||||||
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.0.0" />
|
<PackageReference Include="FlexLabs.EntityFrameworkCore.Upsert" Version="8.0.0" />
|
||||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" />
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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="EFCore.NamingConventions" Version="8.0.3" />
|
||||||
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
|
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
|
||||||
<PackageReference Include="InterpolatedSql.Dapper" Version="2.3.0" />
|
<PackageReference Include="InterpolatedSql.Dapper" Version="2.3.0" />
|
||||||
|
1317
back/src/Kyoo.Postgresql/Migrations/20240324174638_UseDateOnly.Designer.cs
generated
Normal file
1317
back/src/Kyoo.Postgresql/Migrations/20240324174638_UseDateOnly.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -19,12 +19,12 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
{
|
{
|
||||||
#pragma warning disable 612, 618
|
#pragma warning disable 612, 618
|
||||||
modelBuilder
|
modelBuilder
|
||||||
.HasAnnotation("ProductVersion", "7.0.12")
|
.HasAnnotation("ProductVersion", "8.0.3")
|
||||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
.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, "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, "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);
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b =>
|
modelBuilder.Entity("Kyoo.Abstractions.Models.Collection", b =>
|
||||||
@ -109,8 +109,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnType("text")
|
.HasColumnType("text")
|
||||||
.HasColumnName("path");
|
.HasColumnName("path");
|
||||||
|
|
||||||
b.Property<DateTime?>("ReleaseDate")
|
b.Property<DateOnly?>("ReleaseDate")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("date")
|
||||||
.HasColumnName("release_date");
|
.HasColumnName("release_date");
|
||||||
|
|
||||||
b.Property<int?>("Runtime")
|
b.Property<int?>("Runtime")
|
||||||
@ -238,8 +238,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnName("added_date")
|
.HasColumnName("added_date")
|
||||||
.HasDefaultValueSql("now() at time zone 'utc'");
|
.HasDefaultValueSql("now() at time zone 'utc'");
|
||||||
|
|
||||||
b.Property<DateTime?>("AirDate")
|
b.Property<DateOnly?>("AirDate")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("date")
|
||||||
.HasColumnName("air_date");
|
.HasColumnName("air_date");
|
||||||
|
|
||||||
b.Property<string[]>("Aliases")
|
b.Property<string[]>("Aliases")
|
||||||
@ -373,8 +373,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnName("added_date")
|
.HasColumnName("added_date")
|
||||||
.HasDefaultValueSql("now() at time zone 'utc'");
|
.HasDefaultValueSql("now() at time zone 'utc'");
|
||||||
|
|
||||||
b.Property<DateTime?>("EndDate")
|
b.Property<DateOnly?>("EndDate")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("date")
|
||||||
.HasColumnName("end_date");
|
.HasColumnName("end_date");
|
||||||
|
|
||||||
b.Property<string>("ExternalId")
|
b.Property<string>("ExternalId")
|
||||||
@ -404,8 +404,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnType("character varying(256)")
|
.HasColumnType("character varying(256)")
|
||||||
.HasColumnName("slug");
|
.HasColumnName("slug");
|
||||||
|
|
||||||
b.Property<DateTime?>("StartDate")
|
b.Property<DateOnly?>("StartDate")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("date")
|
||||||
.HasColumnName("start_date");
|
.HasColumnName("start_date");
|
||||||
|
|
||||||
b.HasKey("Id")
|
b.HasKey("Id")
|
||||||
@ -440,8 +440,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnType("text[]")
|
.HasColumnType("text[]")
|
||||||
.HasColumnName("aliases");
|
.HasColumnName("aliases");
|
||||||
|
|
||||||
b.Property<DateTime?>("EndAir")
|
b.Property<DateOnly?>("EndAir")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("date")
|
||||||
.HasColumnName("end_air");
|
.HasColumnName("end_air");
|
||||||
|
|
||||||
b.Property<string>("ExternalId")
|
b.Property<string>("ExternalId")
|
||||||
@ -473,8 +473,8 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.HasColumnType("character varying(256)")
|
.HasColumnType("character varying(256)")
|
||||||
.HasColumnName("slug");
|
.HasColumnName("slug");
|
||||||
|
|
||||||
b.Property<DateTime?>("StartAir")
|
b.Property<DateOnly?>("StartAir")
|
||||||
.HasColumnType("timestamp with time zone")
|
.HasColumnType("date")
|
||||||
.HasColumnName("start_air");
|
.HasColumnName("start_air");
|
||||||
|
|
||||||
b.Property<Status>("Status")
|
b.Property<Status>("Status")
|
||||||
@ -651,6 +651,10 @@ namespace Kyoo.Postgresql.Migrations
|
|||||||
.IsUnique()
|
.IsUnique()
|
||||||
.HasDatabaseName("ix_users_slug");
|
.HasDatabaseName("ix_users_slug");
|
||||||
|
|
||||||
|
b.HasIndex("Username")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("ix_users_username");
|
||||||
|
|
||||||
b.ToTable("users", (string)null);
|
b.ToTable("users", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -55,7 +55,7 @@ import arrayShuffle from "array-shuffle";
|
|||||||
import { Tooltip } from "react-tooltip";
|
import { Tooltip } from "react-tooltip";
|
||||||
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
import { getCurrentAccount, readCookie, updateAccount } from "@kyoo/models/src/account-internal";
|
||||||
import { PortalProvider } from "@gorhom/portal";
|
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" });
|
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 layoutInfo = (Component as QueryPage).getLayout ?? (({ page }) => page);
|
||||||
const { Layout, props: layoutProps } =
|
const { Layout, props: layoutProps } =
|
||||||
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
typeof layoutInfo === "function" ? { Layout: layoutInfo, props: {} } : layoutInfo;
|
||||||
return (
|
return <Layout page={<Component {...props} />} randomItems={[]} {...layoutProps} />;
|
||||||
<Layout
|
|
||||||
page={
|
|
||||||
<ErrorContext>
|
|
||||||
<Component {...props} />
|
|
||||||
</ErrorContext>
|
|
||||||
}
|
|
||||||
randomItems={[]}
|
|
||||||
{...layoutProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const App = ({ Component, pageProps }: AppProps) => {
|
const App = ({ Component, pageProps }: AppProps) => {
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { ServerInfoP, User, UserP } from "./resources";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { zdate } from "./utils";
|
import { zdate } from "./utils";
|
||||||
@ -69,7 +69,8 @@ export const ConnectionErrorContext = createContext<{
|
|||||||
error: KyooErrors | null;
|
error: KyooErrors | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
retry?: () => void;
|
retry?: () => void;
|
||||||
}>({ error: null, loading: true });
|
setError: (error: KyooErrors) => void;
|
||||||
|
}>({ error: null, loading: true, setError: () => {} });
|
||||||
|
|
||||||
/* eslint-disable react-hooks/rules-of-hooks */
|
/* eslint-disable react-hooks/rules-of-hooks */
|
||||||
export const AccountProvider = ({
|
export const AccountProvider = ({
|
||||||
@ -96,6 +97,7 @@ export const AccountProvider = ({
|
|||||||
retry: () => {
|
retry: () => {
|
||||||
queryClient.resetQueries({ queryKey: ["auth", "me"] });
|
queryClient.resetQueries({ queryKey: ["auth", "me"] });
|
||||||
},
|
},
|
||||||
|
setError: () => {},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@ -156,15 +158,18 @@ export const AccountProvider = ({
|
|||||||
}
|
}
|
||||||
}, [selected, queryClient]);
|
}, [selected, queryClient]);
|
||||||
|
|
||||||
|
const [permissionError, setPermissionError] = useState<KyooErrors | null>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AccountContext.Provider value={accounts}>
|
<AccountContext.Provider value={accounts}>
|
||||||
<ConnectionErrorContext.Provider
|
<ConnectionErrorContext.Provider
|
||||||
value={{
|
value={{
|
||||||
error: selected ? initialSsrError.current ?? user.error : null,
|
error: selected ? initialSsrError.current ?? user.error ?? permissionError : null,
|
||||||
loading: user.isLoading,
|
loading: user.isLoading,
|
||||||
retry: () => {
|
retry: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
queryClient.invalidateQueries({ queryKey: ["auth", "me"] });
|
||||||
},
|
},
|
||||||
|
setError: setPermissionError,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
@ -49,7 +49,7 @@ export const A = ({
|
|||||||
replace
|
replace
|
||||||
? {
|
? {
|
||||||
nativeBehavior: "stack-replace",
|
nativeBehavior: "stack-replace",
|
||||||
isNestedNavigator: false,
|
isNestedNavigator: true,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@ -105,7 +105,7 @@ export const Link = ({
|
|||||||
const linkProps = useLink({
|
const linkProps = useLink({
|
||||||
href: href ?? "#",
|
href: href ?? "#",
|
||||||
replace,
|
replace,
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
// @ts-ignore Missing hrefAttrs type definition.
|
// @ts-ignore Missing hrefAttrs type definition.
|
||||||
linkProps.hrefAttrs = { ...linkProps.hrefAttrs, target };
|
linkProps.hrefAttrs = { ...linkProps.hrefAttrs, target };
|
||||||
|
@ -335,7 +335,7 @@ export const EpisodeLine = ({
|
|||||||
{isLoading || <P numberOfLines={descriptionExpanded ? undefined : 3}>{overview}</P>}
|
{isLoading || <P numberOfLines={descriptionExpanded ? undefined : 3}>{overview}</P>}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<IconButton
|
<IconButton
|
||||||
{...css(["more"])}
|
{...css(["more", Platform.OS !== "web" && { opacity: 1 }])}
|
||||||
icon={descriptionExpanded ? ExpandLess : ExpandMore}
|
icon={descriptionExpanded ? ExpandLess : ExpandMore}
|
||||||
{...tooltip(t(descriptionExpanded ? "misc.collapse" : "misc.expand"))}
|
{...tooltip(t(descriptionExpanded ? "misc.collapse" : "misc.expand"))}
|
||||||
onPress={(e) => {
|
onPress={(e) => {
|
||||||
|
@ -18,21 +18,49 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ConnectionErrorContext } from "@kyoo/models";
|
import { ConnectionErrorContext, useAccount } from "@kyoo/models";
|
||||||
import { Button, H1, P, ts } from "@kyoo/primitives";
|
import { Button, H1, Icon, Link, P, ts } from "@kyoo/primitives";
|
||||||
import { useRouter } from "solito/router";
|
import { useRouter } from "solito/router";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { DefaultLayout } from "../layout";
|
import { DefaultLayout } from "../layout";
|
||||||
|
import { ErrorView } from "./error";
|
||||||
|
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
|
||||||
|
|
||||||
export const ConnectionError = () => {
|
export const ConnectionError = () => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { error, retry } = useContext(ConnectionErrorContext);
|
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 (
|
return (
|
||||||
<View {...css({ padding: ts(2) })}>
|
<View {...css({ padding: ts(2) })}>
|
||||||
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
|
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
|
||||||
|
@ -18,19 +18,11 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* 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 { P } from "@kyoo/primitives";
|
||||||
import {
|
import { useContext, useLayoutEffect } from "react";
|
||||||
ReactElement,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useLayoutEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { PermissionError } from "./unauthorized";
|
|
||||||
|
|
||||||
export const ErrorView = ({
|
export const ErrorView = ({
|
||||||
error,
|
error,
|
||||||
@ -40,7 +32,7 @@ export const ErrorView = ({
|
|||||||
noBubble?: boolean;
|
noBubble?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { css } = useYoshiki();
|
const { css } = useYoshiki();
|
||||||
const setError = useErrorContext();
|
const { setError } = useContext(ConnectionErrorContext);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
// if this is a permission error, make it go up the tree to have a whole page login screen.
|
// 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>
|
</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);
|
|
||||||
};
|
|
||||||
|
@ -18,13 +18,10 @@
|
|||||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { KyooErrors, useAccount } from "@kyoo/models";
|
import { P } from "@kyoo/primitives";
|
||||||
import { Button, Icon, Link, P, ts } from "@kyoo/primitives";
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { View } from "react-native";
|
import { View } from "react-native";
|
||||||
import { rem, useYoshiki } from "yoshiki/native";
|
import { useYoshiki } from "yoshiki/native";
|
||||||
import { ErrorView } from "./error";
|
|
||||||
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
|
|
||||||
|
|
||||||
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -43,31 +40,3 @@ export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
|||||||
</View>
|
</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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -46,7 +46,7 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!apiUrl && Platform.OS !== "web")
|
if (!apiUrl && Platform.OS !== "web")
|
||||||
router.replace("/server-url", undefined, {
|
router.replace("/server-url", undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
}, [apiUrl, router]);
|
}, [apiUrl, router]);
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ export const LoginPage: QueryPage<{ apiUrl?: string; error?: string }> = ({
|
|||||||
setError(error);
|
setError(error);
|
||||||
if (error) return;
|
if (error) return;
|
||||||
router.replace("/", undefined, {
|
router.replace("/", undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
{...css({
|
{...css({
|
||||||
|
@ -106,7 +106,7 @@ export const OidcCallbackPage: QueryPage<{
|
|||||||
|
|
||||||
function onError(error: string) {
|
function onError(error: string) {
|
||||||
router.replace({ pathname: "/login", query: { error, apiUrl } }, undefined, {
|
router.replace({ pathname: "/login", query: { error, apiUrl } }, undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
async function run() {
|
async function run() {
|
||||||
@ -114,7 +114,7 @@ export const OidcCallbackPage: QueryPage<{
|
|||||||
if (loginError) onError(loginError);
|
if (loginError) onError(loginError);
|
||||||
else {
|
else {
|
||||||
router.replace("/", undefined, {
|
router.replace("/", undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!apiUrl && Platform.OS !== "web")
|
if (!apiUrl && Platform.OS !== "web")
|
||||||
router.replace("/server-url", undefined, {
|
router.replace("/server-url", undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
}, [apiUrl, router]);
|
}, [apiUrl, router]);
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ export const RegisterPage: QueryPage<{ apiUrl?: string }> = ({ apiUrl }) => {
|
|||||||
setError(error);
|
setError(error);
|
||||||
if (error) return;
|
if (error) return;
|
||||||
router.replace("/", undefined, {
|
router.replace("/", undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
{...css({
|
{...css({
|
||||||
|
@ -157,11 +157,11 @@ export const Player = ({
|
|||||||
if (!data) return;
|
if (!data) return;
|
||||||
if (data.type === "movie")
|
if (data.type === "movie")
|
||||||
router.replace(`/movie/${data.slug}`, undefined, {
|
router.replace(`/movie/${data.slug}`, undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
else
|
else
|
||||||
router.replace(next ?? `/show/${data.show!.slug}`, undefined, {
|
router.replace(next ?? `/show/${data.show!.slug}`, undefined, {
|
||||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: false },
|
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
{...css(StyleSheet.absoluteFillObject)}
|
{...css(StyleSheet.absoluteFillObject)}
|
||||||
|
@ -220,7 +220,7 @@ export const Video = memo(function Video({
|
|||||||
onLoad={(info) => {
|
onLoad={(info) => {
|
||||||
setDuration(info.duration);
|
setDuration(info.duration);
|
||||||
}}
|
}}
|
||||||
onPlayPause={setPlay}
|
onPlaybackStateChanged={(state) => setPlay(state.isPlaying)}
|
||||||
fonts={fonts}
|
fonts={fonts}
|
||||||
subtitles={subtitles}
|
subtitles={subtitles}
|
||||||
onMediaUnsupported={() => {
|
onMediaUnsupported={() => {
|
||||||
|
@ -24,7 +24,6 @@ declare module "react-native-video" {
|
|||||||
interface ReactVideoProps {
|
interface ReactVideoProps {
|
||||||
fonts?: string[];
|
fonts?: string[];
|
||||||
subtitles?: Subtitle[];
|
subtitles?: Subtitle[];
|
||||||
onPlayPause: (isPlaying: boolean) => void;
|
|
||||||
onMediaUnsupported?: () => void;
|
onMediaUnsupported?: () => void;
|
||||||
}
|
}
|
||||||
export type VideoProps = Omit<ReactVideoProps, "source"> & {
|
export type VideoProps = Omit<ReactVideoProps, "source"> & {
|
||||||
@ -103,13 +102,18 @@ const Video = forwardRef<VideoRef, VideoProps>(function Video(
|
|||||||
onLoad?.(info);
|
onLoad?.(info);
|
||||||
}}
|
}}
|
||||||
onBuffer={onBuffer}
|
onBuffer={onBuffer}
|
||||||
onError={onMediaUnsupported}
|
onError={(error) => {
|
||||||
|
console.error(error);
|
||||||
|
if (mode === PlayMode.Direct) onMediaUnsupported?.();
|
||||||
|
else onError?.(error);
|
||||||
|
}}
|
||||||
selectedVideoTrack={
|
selectedVideoTrack={
|
||||||
video === -1
|
video === -1
|
||||||
? { type: SelectedVideoTrackType.AUDO }
|
? { type: SelectedVideoTrackType.AUDO }
|
||||||
: { type: SelectedVideoTrackType.RESOLUTION, value: video }
|
: { 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) => ({
|
textTracks={subtitles?.map((x) => ({
|
||||||
type: MimeTypes.get(x.codec) as any,
|
type: MimeTypes.get(x.codec) as any,
|
||||||
uri: x.link!,
|
uri: x.link!,
|
||||||
|
@ -115,7 +115,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
onProgress,
|
onProgress,
|
||||||
onError,
|
onError,
|
||||||
onEnd,
|
onEnd,
|
||||||
onPlayPause,
|
onPlaybackStateChanged,
|
||||||
onMediaUnsupported,
|
onMediaUnsupported,
|
||||||
fonts,
|
fonts,
|
||||||
},
|
},
|
||||||
@ -182,7 +182,7 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
if (!hls) return;
|
if (!hls) return;
|
||||||
const update = () => {
|
const update = () => {
|
||||||
if (!hls) return;
|
if (!hls) return;
|
||||||
hls.audioTrack = audio.index;
|
hls.audioTrack = audio?.index ?? 0;
|
||||||
};
|
};
|
||||||
update();
|
update();
|
||||||
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, update);
|
hls.on(Hls.Events.AUDIO_TRACKS_UPDATED, update);
|
||||||
@ -234,9 +234,8 @@ const Video = forwardRef<{ seek: (value: number) => void }, VideoProps>(function
|
|||||||
onLoadedMetadata={() => {
|
onLoadedMetadata={() => {
|
||||||
if (source.startPosition) setProgress(source.startPosition / 1000);
|
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={() => onPlaybackStateChanged?.({ isPlaying: true })}
|
||||||
// onPlay={() => onPlayPause?.call(null, true)}
|
onPause={() => onPlaybackStateChanged?.({ isPlaying: false })}
|
||||||
// onPause={() => onPlayPause?.call(null, false)}
|
|
||||||
onEnded={onEnd}
|
onEnded={onEnd}
|
||||||
{...css({ width: "100%", height: "100%", objectFit: "contain" })}
|
{...css({ width: "100%", height: "100%", objectFit: "contain" })}
|
||||||
/>
|
/>
|
||||||
|
@ -9,13 +9,12 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/zoriya/go-mediainfo"
|
"github.com/zoriya/go-mediainfo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaInfo struct {
|
type MediaInfo struct {
|
||||||
// closed if the mediainfo is ready for read. open otherwise
|
|
||||||
ready <-chan struct{}
|
|
||||||
// The sha1 of the video file.
|
// The sha1 of the video file.
|
||||||
Sha string `json:"sha"`
|
Sha string `json:"sha"`
|
||||||
/// The internal path of the video file.
|
/// The internal path of the video file.
|
||||||
@ -177,37 +176,38 @@ var SubtitleExtensions = map[string]string{
|
|||||||
"vtt": "vtt",
|
"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) {
|
func GetInfo(path string, sha string, route string) (*MediaInfo, error) {
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
ret, _ := infos.GetOrCreate(sha, func() *MediaInfo {
|
ret, _ := infos.GetOrCreate(sha, func() *MICache {
|
||||||
readyChan := make(chan struct{})
|
mi := &MICache{info: &MediaInfo{Sha: sha}}
|
||||||
mi := &MediaInfo{
|
mi.ready.Add(1)
|
||||||
Sha: sha,
|
|
||||||
ready: readyChan,
|
|
||||||
}
|
|
||||||
go func() {
|
go func() {
|
||||||
save_path := fmt.Sprintf("%s/%s/info.json", Settings.Metadata, sha)
|
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)
|
log.Printf("Using mediainfo cache on filesystem for %s", path)
|
||||||
close(readyChan)
|
mi.ready.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var val *MediaInfo
|
var val *MediaInfo
|
||||||
val, err = getInfo(path, route)
|
val, err = getInfo(path, route)
|
||||||
*mi = *val
|
*mi.info = *val
|
||||||
mi.ready = readyChan
|
mi.info.Sha = sha
|
||||||
mi.Sha = sha
|
mi.ready.Done()
|
||||||
close(readyChan)
|
saveInfo(save_path, mi.info)
|
||||||
saveInfo(save_path, mi)
|
|
||||||
}()
|
}()
|
||||||
return mi
|
return mi
|
||||||
})
|
})
|
||||||
<-ret.ready
|
ret.ready.Wait()
|
||||||
return ret, err
|
return ret.info, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSavedInfo[T any](save_path string, mi *T) error {
|
func getSavedInfo[T any](save_path string, mi *T) error {
|
||||||
|
@ -15,14 +15,17 @@ type Keyframe struct {
|
|||||||
Keyframes []float64
|
Keyframes []float64
|
||||||
CanTransmux bool
|
CanTransmux bool
|
||||||
IsDone bool
|
IsDone bool
|
||||||
mutex sync.RWMutex
|
info *KeyframeInfo
|
||||||
ready sync.WaitGroup
|
}
|
||||||
listeners []func(keyframes []float64)
|
type KeyframeInfo struct {
|
||||||
|
mutex sync.RWMutex
|
||||||
|
ready sync.WaitGroup
|
||||||
|
listeners []func(keyframes []float64)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kf *Keyframe) Get(idx int32) float64 {
|
func (kf *Keyframe) Get(idx int32) float64 {
|
||||||
kf.mutex.RLock()
|
kf.info.mutex.RLock()
|
||||||
defer kf.mutex.RUnlock()
|
defer kf.info.mutex.RUnlock()
|
||||||
return kf.Keyframes[idx]
|
return kf.Keyframes[idx]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,8 +33,8 @@ func (kf *Keyframe) Slice(start int32, end int32) []float64 {
|
|||||||
if end <= start {
|
if end <= start {
|
||||||
return []float64{}
|
return []float64{}
|
||||||
}
|
}
|
||||||
kf.mutex.RLock()
|
kf.info.mutex.RLock()
|
||||||
defer kf.mutex.RUnlock()
|
defer kf.info.mutex.RUnlock()
|
||||||
ref := kf.Keyframes[start:end]
|
ref := kf.Keyframes[start:end]
|
||||||
ret := make([]float64, end-start)
|
ret := make([]float64, end-start)
|
||||||
copy(ret, ref)
|
copy(ret, ref)
|
||||||
@ -39,24 +42,24 @@ func (kf *Keyframe) Slice(start int32, end int32) []float64 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (kf *Keyframe) Length() (int32, bool) {
|
func (kf *Keyframe) Length() (int32, bool) {
|
||||||
kf.mutex.RLock()
|
kf.info.mutex.RLock()
|
||||||
defer kf.mutex.RUnlock()
|
defer kf.info.mutex.RUnlock()
|
||||||
return int32(len(kf.Keyframes)), kf.IsDone
|
return int32(len(kf.Keyframes)), kf.IsDone
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kf *Keyframe) add(values []float64) {
|
func (kf *Keyframe) add(values []float64) {
|
||||||
kf.mutex.Lock()
|
kf.info.mutex.Lock()
|
||||||
defer kf.mutex.Unlock()
|
defer kf.info.mutex.Unlock()
|
||||||
kf.Keyframes = append(kf.Keyframes, values...)
|
kf.Keyframes = append(kf.Keyframes, values...)
|
||||||
for _, listener := range kf.listeners {
|
for _, listener := range kf.info.listeners {
|
||||||
listener(kf.Keyframes)
|
listener(kf.Keyframes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (kf *Keyframe) AddListener(callback func(keyframes []float64)) {
|
func (kf *Keyframe) AddListener(callback func(keyframes []float64)) {
|
||||||
kf.mutex.Lock()
|
kf.info.mutex.Lock()
|
||||||
defer kf.mutex.Unlock()
|
defer kf.info.mutex.Unlock()
|
||||||
kf.listeners = append(kf.listeners, callback)
|
kf.info.listeners = append(kf.info.listeners, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyframes = NewCMap[string, *Keyframe]()
|
var keyframes = NewCMap[string, *Keyframe]()
|
||||||
@ -66,13 +69,14 @@ func GetKeyframes(sha string, path string) *Keyframe {
|
|||||||
kf := &Keyframe{
|
kf := &Keyframe{
|
||||||
Sha: sha,
|
Sha: sha,
|
||||||
IsDone: false,
|
IsDone: false,
|
||||||
|
info: &KeyframeInfo{},
|
||||||
}
|
}
|
||||||
kf.ready.Add(1)
|
kf.info.ready.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
save_path := fmt.Sprintf("%s/%s/keyframes.json", Settings.Metadata, sha)
|
save_path := fmt.Sprintf("%s/%s/keyframes.json", Settings.Metadata, sha)
|
||||||
if err := getSavedInfo(save_path, kf); err == nil {
|
if err := getSavedInfo(save_path, kf); err == nil {
|
||||||
log.Printf("Using keyframes cache on filesystem for %s", path)
|
log.Printf("Using keyframes cache on filesystem for %s", path)
|
||||||
kf.ready.Done()
|
kf.info.ready.Done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,7 +87,7 @@ func GetKeyframes(sha string, path string) *Keyframe {
|
|||||||
}()
|
}()
|
||||||
return kf
|
return kf
|
||||||
})
|
})
|
||||||
ret.ready.Wait()
|
ret.info.ready.Wait()
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +158,7 @@ func getKeyframes(path string, kf *Keyframe) error {
|
|||||||
if len(ret) == max {
|
if len(ret) == max {
|
||||||
kf.add(ret)
|
kf.add(ret)
|
||||||
if done == 0 {
|
if done == 0 {
|
||||||
kf.ready.Done()
|
kf.info.ready.Done()
|
||||||
} else if done >= 500 {
|
} else if done >= 500 {
|
||||||
max = 500
|
max = 500
|
||||||
}
|
}
|
||||||
@ -165,7 +169,7 @@ func getKeyframes(path string, kf *Keyframe) error {
|
|||||||
}
|
}
|
||||||
kf.add(ret)
|
kf.add(ret)
|
||||||
if done == 0 {
|
if done == 0 {
|
||||||
kf.ready.Done()
|
kf.info.ready.Done()
|
||||||
}
|
}
|
||||||
kf.IsDone = true
|
kf.IsDone = true
|
||||||
return nil
|
return nil
|
||||||
|
Loading…
x
Reference in New Issue
Block a user