mirror of
https://github.com/zoriya/kyoo-csharp.git
synced 2025-12-06 01:26:10 +00:00
Initial copy
This commit is contained in:
18
.config/dotnet-tools.json
Normal file
18
.config/dotnet-tools.json
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"isRoot": true,
|
||||||
|
"tools": {
|
||||||
|
"dotnet-ef": {
|
||||||
|
"version": "8.0.21",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-ef"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"csharpier": {
|
||||||
|
"version": "0.28.2",
|
||||||
|
"commands": [
|
||||||
|
"dotnet-csharpier"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
.dockerignore
Normal file
17
.dockerignore
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
Dockerfile
|
||||||
|
Dockerfile.dev
|
||||||
|
Dockerfile.*
|
||||||
|
.dockerignore
|
||||||
|
.gitignore
|
||||||
|
docker-compose.yml
|
||||||
|
README.md
|
||||||
|
**/build
|
||||||
|
**/dist
|
||||||
|
**/bin
|
||||||
|
**/obj
|
||||||
|
out
|
||||||
|
docs
|
||||||
|
tests
|
||||||
|
front
|
||||||
|
video
|
||||||
|
nginx.conf.template
|
||||||
99
.editorconfig
Normal file
99
.editorconfig
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
root = false
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = tab
|
||||||
|
indent_size = tab
|
||||||
|
smart_tab = true
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
csharp_prefer_braces = false
|
||||||
|
dotnet_diagnostic.IDE0046.severity = none
|
||||||
|
dotnet_diagnostic.IDE0055.severity = none
|
||||||
|
dotnet_diagnostic.IDE0058.severity = none
|
||||||
|
dotnet_diagnostic.IDE0130.severity = none
|
||||||
|
|
||||||
|
# Convert to file-scoped namespace
|
||||||
|
csharp_style_namespace_declarations = file_scoped:warning
|
||||||
|
# Sort using and Import directives with System.* appearing first
|
||||||
|
dotnet_sort_system_directives_first = true
|
||||||
|
csharp_using_directive_placement = outside_namespace:warning
|
||||||
|
# Avoid "this." if not necessary
|
||||||
|
dotnet_style_qualification_for_field = false:suggestion
|
||||||
|
dotnet_style_qualification_for_property = false:suggestion
|
||||||
|
dotnet_style_qualification_for_method = false:suggestion
|
||||||
|
dotnet_style_qualification_for_event = false:suggestion
|
||||||
|
# Use language keywords instead of framework type names for type references
|
||||||
|
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||||
|
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||||
|
# Suggest more modern language features when available
|
||||||
|
dotnet_style_object_initializer = true:suggestion
|
||||||
|
dotnet_style_collection_initializer = true:suggestion
|
||||||
|
dotnet_style_coalesce_expression = true:suggestion
|
||||||
|
dotnet_style_null_propagation = true:suggestion
|
||||||
|
dotnet_style_explicit_tuple_names = true:suggestion
|
||||||
|
csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion
|
||||||
|
csharp_style_pattern_matching_over_as_with_null_check = true:suggestion
|
||||||
|
csharp_style_inlined_variable_declaration = true:suggestion
|
||||||
|
csharp_style_conditional_delegate_call = true:suggestion
|
||||||
|
dotnet_style_prefer_auto_properties = true
|
||||||
|
dotnet_style_prefer_conditional_expression_over_assignment = true
|
||||||
|
dotnet_style_prefer_conditional_expression_over_return = true
|
||||||
|
# Disable strange throw.
|
||||||
|
csharp_style_throw_expression = false:suggestion
|
||||||
|
# Forbid "var" everywhere
|
||||||
|
csharp_style_var_for_built_in_types = false:suggestion
|
||||||
|
csharp_style_var_when_type_is_apparent = false:suggestion
|
||||||
|
csharp_style_var_elsewhere = false:suggestion
|
||||||
|
# Prefer method-like constructs to have a block body
|
||||||
|
csharp_style_expression_bodied_methods = false:none
|
||||||
|
csharp_style_expression_bodied_constructors = false:none
|
||||||
|
csharp_style_expression_bodied_operators = false:none
|
||||||
|
# Prefer property-like constructs to have an expression-body
|
||||||
|
csharp_style_expression_bodied_properties = true:none
|
||||||
|
csharp_style_expression_bodied_indexers = true:none
|
||||||
|
csharp_style_expression_bodied_accessors = true:none
|
||||||
|
# Newline settings
|
||||||
|
csharp_new_line_before_open_brace = all
|
||||||
|
csharp_new_line_before_else = true
|
||||||
|
csharp_new_line_before_catch = true
|
||||||
|
csharp_new_line_before_finally = true
|
||||||
|
csharp_new_line_before_members_in_object_initializers = false
|
||||||
|
csharp_new_line_before_members_in_anonymous_types = true
|
||||||
|
# Indentation settings
|
||||||
|
csharp_indent_case_contents = true
|
||||||
|
csharp_indent_switch_labels = true
|
||||||
|
# Modifiers
|
||||||
|
dotnet_style_readonly_field = true:suggestion
|
||||||
|
dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion
|
||||||
|
# Naming style
|
||||||
|
dotnet_naming_symbols.privates.applicable_kinds = property,method,event,delegate
|
||||||
|
dotnet_naming_symbols.privates.applicable_accessibilities = private
|
||||||
|
dotnet_naming_style.underscore_pascal.capitalization = pascal_case
|
||||||
|
dotnet_naming_style.underscore_pascal.required_prefix = _
|
||||||
|
dotnet_naming_rule.privates_with_underscore.symbols = privates
|
||||||
|
dotnet_naming_rule.privates_with_underscore.style = underscore_pascal
|
||||||
|
dotnet_naming_rule.privates_with_underscore.severity = warning
|
||||||
|
dotnet_diagnostic.IDE1006.severity = warning
|
||||||
|
# ReSharper properties
|
||||||
|
resharper_align_multiline_binary_expressions_chain = false
|
||||||
|
resharper_csharp_empty_block_style = together_same_line
|
||||||
|
resharper_indent_nested_foreach_stmt = true
|
||||||
|
resharper_indent_nested_for_stmt = true
|
||||||
|
resharper_indent_nested_while_stmt = true
|
||||||
|
resharper_keep_existing_embedded_arrangement = false
|
||||||
|
resharper_place_accessorholder_attribute_on_same_line = true
|
||||||
|
resharper_place_simple_embedded_statement_on_same_line = false
|
||||||
|
resharper_wrap_before_arrow_with_expressions = true
|
||||||
|
resharper_xmldoc_attribute_indent = align_by_first_attribute
|
||||||
|
resharper_xmldoc_indent_child_elements = RemoveIndent
|
||||||
|
resharper_xmldoc_indent_text = RemoveIndent
|
||||||
|
# Switch on enum
|
||||||
|
dotnet_diagnostic.CS8509.severity=error # missing switch case for named enum value
|
||||||
|
dotnet_diagnostic.CS8524.severity=none # missing switch case for unnamed enum value
|
||||||
|
|
||||||
|
# Waiting for https://github.com/dotnet/roslyn/issues/44596 to get fixed.
|
||||||
|
# file_header_template = Kyoo - A portable and vast media library solution.\nCopyright (c) Kyoo.\n\nSee AUTHORS.md and LICENSE file in the project root for full license information.\n\nKyoo is free software: you can redistribute it and/or modify\nit under the terms of the GNU General Public License as published by\nthe Free Software Foundation, either version 3 of the License, or\nany later version.\n\nKyoo is distributed in the hope that it will be useful,\nbut WITHOUT ANY WARRANTY; without even the implied warranty of\nMERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\nGNU General Public License for more details.\n\nYou should have received a copy of the GNU General Public License\nalong with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
37
.env.example
Normal file
37
.env.example
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# vi: ft=sh
|
||||||
|
# shellcheck disable=SC2034
|
||||||
|
|
||||||
|
# http route prefix (will listen to $KYOO_PREFIX/movie for example)
|
||||||
|
KYOO_PREFIX=""
|
||||||
|
|
||||||
|
|
||||||
|
# Optional authentication settings
|
||||||
|
# Set to true to disable login with password (OIDC auth must be configured)
|
||||||
|
# AUTHENTICATION_DISABLE_PASSWORD_LOGIN=true
|
||||||
|
# Set to true to disable the creation of new users (OIDC auth must be configured)
|
||||||
|
# AUTHENTICATION_DISABLE_USER_REGISTRATION=true
|
||||||
|
|
||||||
|
# Postgres settings
|
||||||
|
# POSTGRES_URL=postgres://user:password@hostname:port/dbname?sslmode=verify-full&sslrootcert=/path/to/server.crt&sslcert=/path/to/client.crt&sslkey=/path/to/client.key
|
||||||
|
# The behavior of the below variables match what is documented here:
|
||||||
|
# https://www.postgresql.org/docs/current/libpq-envars.html
|
||||||
|
PGUSER=kyoo
|
||||||
|
PGPASSWORD=password
|
||||||
|
PGDB=kyooDB
|
||||||
|
PGSERVER=postgres
|
||||||
|
PGPORT=5432
|
||||||
|
# PGOPTIONS=-c search_path=kyoo,public
|
||||||
|
# PGPASSFILE=/my/password # Takes precedence over PGPASSWORD. New line characters are not trimmed.
|
||||||
|
# PGSSLMODE=verify-full
|
||||||
|
# PGSSLROOTCERT=/my/serving.crt
|
||||||
|
# PGSSLCERT=/my/client.crt
|
||||||
|
# PGSSLKEY=/my/client.key
|
||||||
|
|
||||||
|
# RabbitMQ settings
|
||||||
|
# Full list of options: https://www.rabbitmq.com/uri-spec.html, https://www.rabbitmq.com/docs/uri-query-parameters
|
||||||
|
# RABBITMQ_URL=amqps://user:password@rabbitmq-server:1234/vhost?cacertfile=/path/to/cacert.pem&certfile=/path/to/cert.pem&keyfile=/path/to/key.pem&verify=verify_peer&auth_mechanism=EXTERNAL
|
||||||
|
# These values override what is provided the the URL variable
|
||||||
|
RABBITMQ_DEFAULT_USER=guest
|
||||||
|
RABBITMQ_DEFAULT_PASS=guest
|
||||||
|
RABBITMQ_HOST=rabbitmq
|
||||||
|
RABBITMQ_PORT=5672
|
||||||
352
.gitignore
vendored
Normal file
352
.gitignore
vendored
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
out
|
||||||
|
libtranscoder.so
|
||||||
|
libtranscoder.dylib
|
||||||
|
transcoder.dll
|
||||||
|
kyoo_datadir
|
||||||
|
|
||||||
|
video
|
||||||
|
.env
|
||||||
|
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
##
|
||||||
|
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.rsuser
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
[Aa][Rr][Mm]/
|
||||||
|
[Aa][Rr][Mm]64/
|
||||||
|
bld/
|
||||||
|
[Bb]in/
|
||||||
|
[Oo]bj/
|
||||||
|
[Ll]og/
|
||||||
|
|
||||||
|
# Visual Studio 2015/2017 cache/options directory
|
||||||
|
.vs/
|
||||||
|
# Uncomment if you have tasks that create the project's static files in wwwroot
|
||||||
|
#wwwroot/
|
||||||
|
|
||||||
|
# Visual Studio 2017 auto generated files
|
||||||
|
Generated\ Files/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
# Benchmark Results
|
||||||
|
BenchmarkDotNet.Artifacts/
|
||||||
|
|
||||||
|
# .NET Core
|
||||||
|
project.lock.json
|
||||||
|
project.fragment.lock.json
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
# StyleCop
|
||||||
|
StyleCopReport.xml
|
||||||
|
|
||||||
|
# Files built by Visual Studio
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_h.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.iobj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.ipdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*_wpftmp.csproj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
*.VC.db
|
||||||
|
*.VC.VC.opendb
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# Visual Studio Trace Files
|
||||||
|
*.e2e
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# AxoCover is a Code Coverage Tool
|
||||||
|
.axoCover/*
|
||||||
|
!.axoCover/settings.json
|
||||||
|
|
||||||
|
# Visual Studio code coverage results
|
||||||
|
*.coverage
|
||||||
|
*.coveragexml
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# Note: Comment the next line if you want to checkin your web deploy settings,
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# Microsoft Azure Web App publish settings. Comment the next line if you want to
|
||||||
|
# checkin your Azure Web App publish settings, but sensitive information contained
|
||||||
|
# in these scripts will be unencrypted
|
||||||
|
PublishScripts/
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/[Pp]ackages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/[Pp]ackages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/[Pp]ackages/repositories.config
|
||||||
|
# NuGet v3's project.json files produces more ignorable files
|
||||||
|
*.nuget.props
|
||||||
|
*.nuget.targets
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Windows Store app package directories and files
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
Package.StoreAssociation.xml
|
||||||
|
_pkginfo.txt
|
||||||
|
*.appx
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!?*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.jfm
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
# Including strong name files can present a security risk
|
||||||
|
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
|
||||||
|
#*.snk
|
||||||
|
|
||||||
|
# Since there are multiple workflows, uncomment next line to ignore bower_components
|
||||||
|
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
|
||||||
|
#bower_components/
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
ServiceFabricBackup/
|
||||||
|
*.rptproj.bak
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
*.ndf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
*.rptproj.rsuser
|
||||||
|
*- Backup*.rdl
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
node_modules/
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
|
||||||
|
*.vbw
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
paket-files/
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
|
|
||||||
|
# JetBrains Rider
|
||||||
|
.idea/
|
||||||
|
*.sln.iml
|
||||||
|
|
||||||
|
# CodeRush personal settings
|
||||||
|
.cr/personal
|
||||||
|
|
||||||
|
# Python Tools for Visual Studio (PTVS)
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Cake - Uncomment if you are using it
|
||||||
|
# tools/**
|
||||||
|
# !tools/packages.config
|
||||||
|
|
||||||
|
# Tabs Studio
|
||||||
|
*.tss
|
||||||
|
|
||||||
|
# Telerik's JustMock configuration file
|
||||||
|
*.jmconfig
|
||||||
|
|
||||||
|
# BizTalk build output
|
||||||
|
*.btp.cs
|
||||||
|
*.btm.cs
|
||||||
|
*.odx.cs
|
||||||
|
*.xsd.cs
|
||||||
|
|
||||||
|
# OpenCover UI analysis results
|
||||||
|
OpenCover/
|
||||||
|
|
||||||
|
# Azure Stream Analytics local run output
|
||||||
|
ASALocalRun/
|
||||||
|
|
||||||
|
# MSBuild Binary and Structured Log
|
||||||
|
*.binlog
|
||||||
|
|
||||||
|
# NVidia Nsight GPU debugger configuration file
|
||||||
|
*.nvuser
|
||||||
|
|
||||||
|
# MFractors (Xamarin productivity tool) working folder
|
||||||
|
.mfractor/
|
||||||
|
|
||||||
|
# Local History for Visual Studio
|
||||||
|
.localhistory/
|
||||||
|
|
||||||
|
# BeatPulse healthcheck temp database
|
||||||
|
healthchecksdb
|
||||||
|
/Kyoo/TheTVDB-Credentials.json
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
.netcoredbg_hist
|
||||||
29
Dockerfile
Normal file
29
Dockerfile
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 as builder
|
||||||
|
ARG TARGETARCH
|
||||||
|
WORKDIR /kyoo
|
||||||
|
|
||||||
|
COPY Kyoo.sln ./Kyoo.sln
|
||||||
|
COPY nuget.config ./nuget.config
|
||||||
|
COPY src/Directory.Build.props src/Directory.Build.props
|
||||||
|
COPY src/Kyoo.Authentication/Kyoo.Authentication.csproj src/Kyoo.Authentication/Kyoo.Authentication.csproj
|
||||||
|
COPY src/Kyoo.Abstractions/Kyoo.Abstractions.csproj src/Kyoo.Abstractions/Kyoo.Abstractions.csproj
|
||||||
|
COPY src/Kyoo.Core/Kyoo.Core.csproj src/Kyoo.Core/Kyoo.Core.csproj
|
||||||
|
COPY src/Kyoo.Postgresql/Kyoo.Postgresql.csproj src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
|
||||||
|
COPY src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
|
||||||
|
COPY src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj
|
||||||
|
COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj
|
||||||
|
RUN dotnet restore -a $TARGETARCH
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
ARG VERSION
|
||||||
|
RUN dotnet publish -a $TARGETARCH --no-restore -c Release -o /app "-p:Version=${VERSION:-"0.0.0-dev"}" src/Kyoo.Core
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||||
|
RUN apt-get update && apt-get install -y curl
|
||||||
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 5000
|
||||||
|
# The back can take a long time to start if meilisearch is initializing
|
||||||
|
HEALTHCHECK --interval=30s --retries=15 CMD curl --fail http://localhost:5000/health || exit
|
||||||
|
ENTRYPOINT ["/app/kyoo"]
|
||||||
22
Dockerfile.dev
Normal file
22
Dockerfile.dev
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM mcr.microsoft.com/dotnet/sdk:8.0
|
||||||
|
RUN apt-get update && apt-get install -y curl
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY Kyoo.sln ./Kyoo.sln
|
||||||
|
COPY nuget.config ./nuget.config
|
||||||
|
COPY src/Directory.Build.props src/Directory.Build.props
|
||||||
|
COPY src/Kyoo.Authentication/Kyoo.Authentication.csproj src/Kyoo.Authentication/Kyoo.Authentication.csproj
|
||||||
|
COPY src/Kyoo.Abstractions/Kyoo.Abstractions.csproj src/Kyoo.Abstractions/Kyoo.Abstractions.csproj
|
||||||
|
COPY src/Kyoo.Core/Kyoo.Core.csproj src/Kyoo.Core/Kyoo.Core.csproj
|
||||||
|
COPY src/Kyoo.Postgresql/Kyoo.Postgresql.csproj src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
|
||||||
|
COPY src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
|
||||||
|
COPY src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj
|
||||||
|
COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj
|
||||||
|
RUN dotnet restore
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
EXPOSE 5000
|
||||||
|
ENV DOTNET_USE_POLLING_FILE_WATCHER 1
|
||||||
|
# HEALTHCHECK --interval=30s CMD curl --fail http://localhost:5000/health || exit
|
||||||
|
HEALTHCHECK CMD true
|
||||||
|
ENTRYPOINT ["dotnet", "watch", "--non-interactive", "run", "--no-restore", "--project", "/app/src/Kyoo.Core"]
|
||||||
30
Dockerfile.migrations
Normal file
30
Dockerfile.migrations
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:8.0 as builder
|
||||||
|
ARG TARGETARCH
|
||||||
|
WORKDIR /kyoo
|
||||||
|
|
||||||
|
COPY .config/dotnet-tools.json .config/dotnet-tools.json
|
||||||
|
RUN dotnet tool restore
|
||||||
|
|
||||||
|
COPY Kyoo.sln ./Kyoo.sln
|
||||||
|
COPY nuget.config ./nuget.config
|
||||||
|
COPY src/Directory.Build.props src/Directory.Build.props
|
||||||
|
COPY src/Kyoo.Authentication/Kyoo.Authentication.csproj src/Kyoo.Authentication/Kyoo.Authentication.csproj
|
||||||
|
COPY src/Kyoo.Abstractions/Kyoo.Abstractions.csproj src/Kyoo.Abstractions/Kyoo.Abstractions.csproj
|
||||||
|
COPY src/Kyoo.Core/Kyoo.Core.csproj src/Kyoo.Core/Kyoo.Core.csproj
|
||||||
|
COPY src/Kyoo.Postgresql/Kyoo.Postgresql.csproj src/Kyoo.Postgresql/Kyoo.Postgresql.csproj
|
||||||
|
COPY src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj src/Kyoo.Meilisearch/Kyoo.Meilisearch.csproj
|
||||||
|
COPY src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj src/Kyoo.RabbitMq/Kyoo.RabbitMq.csproj
|
||||||
|
COPY src/Kyoo.Swagger/Kyoo.Swagger.csproj src/Kyoo.Swagger/Kyoo.Swagger.csproj
|
||||||
|
RUN dotnet restore -a $TARGETARCH
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN dotnet build
|
||||||
|
RUN dotnet ef migrations bundle \
|
||||||
|
--msbuildprojectextensionspath out/obj/Kyoo.Postgresql \
|
||||||
|
--no-build --self-contained -r linux-${TARGETARCH} -f \
|
||||||
|
-o /app/migrate -p src/Kyoo.Postgresql --verbose
|
||||||
|
|
||||||
|
FROM mcr.microsoft.com/dotnet/runtime-deps:8.0
|
||||||
|
COPY --from=builder /app/migrate /app/migrate
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/migrate"]
|
||||||
47
Kyoo.ruleset
Normal file
47
Kyoo.ruleset
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<RuleSet Name="Kyoo" ToolsVersion="10.0">
|
||||||
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.MaintainabilityRules">
|
||||||
|
<Rule Id="SA1413" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
|
||||||
|
<Rule Id="SA1414" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
|
||||||
|
<Rule Id="SA1114" Action="None" /> <!-- UseTrailingCommasInMultiLineInitializers -->
|
||||||
|
</Rules>
|
||||||
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.OrderingRules">
|
||||||
|
<Rule Id="SA1201" Action="None" /> <!-- ElementsMustAppearInTheCorrectOrder -->
|
||||||
|
<Rule Id="SA1202" Action="None" /> <!-- ElementsMustBeOrderedByAccess -->
|
||||||
|
</Rules>
|
||||||
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.NamingRules">
|
||||||
|
<Rule Id="SA1309" Action="None" /> <!-- FieldNamesMustNotBeginWithUnderscore -->
|
||||||
|
<Rule Id="SX1309" Action="Warning" /> <!-- FieldNamesMustBeginWithUnderscore -->
|
||||||
|
<Rule Id="SA1300" Action="None" /> <!-- ElementMustBeginWithUpperCaseLetter (this conflict with the _ prefix for privates, enforced by an IDE rule) -->
|
||||||
|
<Rule Id="SA1316" Action="None" /> <!-- TupleElementNamesShouldUseCorrectCasing (should be camels when deconstructing but pascal otherwise. -->
|
||||||
|
</Rules>
|
||||||
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.ReadabilityRules">
|
||||||
|
<Rule Id="SA1101" Action="None" /> <!-- PrefixLocalCallsWithThis -->
|
||||||
|
<Rule Id="SX1101" Action="Warning" /> <!-- DoNotPrefixLocalMembersWithThis -->
|
||||||
|
<Rule Id="SA1134" Action="None" /> <!-- AttributesMustNotShareLine -->
|
||||||
|
<Rule Id="SA1117" Action="None" /> <!-- ParametersMustBeOnSameLineOrSeparateLines -->
|
||||||
|
<Rule Id="SA1116" Action="None" /> <!-- SplitParametersMustStartOnLineAfterDeclaration -->
|
||||||
|
<Rule Id="SA1111" Action="None" /> <!-- ClosingParenthesisMustBeOnLineOfLastParameter -->
|
||||||
|
<Rule Id="SA1009" Action="None" /> <!-- ClosingParenthesisMustBeSpacedCorrectly (bugged if the parenthesis is on a line by iteself) -->
|
||||||
|
</Rules>
|
||||||
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.SpacingRules">
|
||||||
|
<Rule Id="SA1502" Action="None"/> <!-- DocumentationLinesMustBeginWithSingleSpace -->
|
||||||
|
<Rule Id="SA1027" Action="None"/> <!-- UseTabsCorrectly (smarts tabs are broken). TODO find a way to enable smart tabs -->
|
||||||
|
</Rules>
|
||||||
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.LayoutRules">
|
||||||
|
<Rule Id="SA1402" Action="None"/> <!-- SingleClassPerFile -->
|
||||||
|
<Rule Id="SA1649" Action="None"/> <!-- Class name must be filename -->
|
||||||
|
<Rule Id="SA1503" Action="None"/> <!-- BracesMustNotBeOmitted -->
|
||||||
|
<Rule Id="SA1512" Action="None"/> <!-- Let me comment -->
|
||||||
|
<Rule Id="SA1520" Action="None"/> <!-- UseBracesConsistently -->
|
||||||
|
<Rule Id="SA1515" Action="None"/> <!-- SingleLineCommentMustBePrecededByBlankLine -->
|
||||||
|
<Rule Id="SA1513" Action="None"/> <!-- ClosingBraceMustBeFollowedByBlankLine -->
|
||||||
|
</Rules>
|
||||||
|
<Rules AnalyzerId="StyleCop.Analyzers" RuleNamespace="StyleCop.CSharp.DocumentationRules">
|
||||||
|
<Rule Id="SA1600" Action="None" /> <!-- Elements Shuld be Documented -->
|
||||||
|
<Rule Id="SA1602" Action="None" /> <!-- Enums should be documented -->
|
||||||
|
<Rule Id="SA1642" Action="None" /> <!-- ConstructorSummaryDocumentationMustBeginWithStandardText -->
|
||||||
|
<Rule Id="SA1643" Action="None" /> <!-- DestructorSummaryDocumentationMustBeginWithStandardText -->
|
||||||
|
<Rule Id="SA1623" Action="None" /> <!-- PropertySummaryDocumentationMustMatchAccessors -->
|
||||||
|
<Rule Id="SA1629" Action="None" /> <!-- DocumentationTextMustEndWithAPeriod -->
|
||||||
|
</Rules>
|
||||||
|
</RuleSet>
|
||||||
64
Kyoo.sln
Normal file
64
Kyoo.sln
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
#
|
||||||
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kyoo.Core", "src\Kyoo.Core\Kyoo.Core.csproj", "{0F8275B6-C7DD-42DF-A168-755C81B1C329}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Abstractions", "src\Kyoo.Abstractions\Kyoo.Abstractions.csproj", "{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Postgresql", "src\Kyoo.Postgresql\Kyoo.Postgresql.csproj", "{3213C96D-0BF3-460B-A8B5-B9977229408A}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Authentication", "src\Kyoo.Authentication\Kyoo.Authentication.csproj", "{7A841335-6523-47DB-9717-80AA7BD943FD}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Swagger", "src\Kyoo.Swagger\Kyoo.Swagger.csproj", "{7D1A7596-73F6-4D35-842E-A5AD9C620596}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Meilisearch", "src\Kyoo.Meilisearch\Kyoo.Meilisearch.csproj", "{F8E6018A-FD51-40EB-99FF-A26BA59F2762}"
|
||||||
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.RabbitMq", "src\Kyoo.RabbitMq\Kyoo.RabbitMq.csproj", "{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}"
|
||||||
|
EndProject
|
||||||
|
Global
|
||||||
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{0F8275B6-C7DD-42DF-A168-755C81B1C329}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{0F8275B6-C7DD-42DF-A168-755C81B1C329}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{0F8275B6-C7DD-42DF-A168-755C81B1C329}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{0F8275B6-C7DD-42DF-A168-755C81B1C329}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{3213C96D-0BF3-460B-A8B5-B9977229408A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7A841335-6523-47DB-9717-80AA7BD943FD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7A841335-6523-47DB-9717-80AA7BD943FD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{6515380E-1E57-42DA-B6E3-E1C8A848818A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{2374D500-1ADB-4752-85DB-8BB0DDF5A8E8}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{4FF1ECD9-6EEF-4440-B037-A661D78FB04D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{4FF1ECD9-6EEF-4440-B037-A661D78FB04D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{4FF1ECD9-6EEF-4440-B037-A661D78FB04D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{4FF1ECD9-6EEF-4440-B037-A661D78FB04D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{7D1A7596-73F6-4D35-842E-A5AD9C620596}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{F8E6018A-FD51-40EB-99FF-A26BA59F2762}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{F8E6018A-FD51-40EB-99FF-A26BA59F2762}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{F8E6018A-FD51-40EB-99FF-A26BA59F2762}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{F8E6018A-FD51-40EB-99FF-A26BA59F2762}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{B97AD4A8-E6E6-41CD-87DF-5F1326FD7198}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Kyoo backend
|
||||||
|
|
||||||
|
This is a copy of the v4's backend of kyoo, mainly used for easy access now that the back was entirely rewritten for v5.
|
||||||
|
|
||||||
|
If you wanna see the history of the tree, you can go to the main repo [here](https://github.com/zoriya/kyoo)
|
||||||
4
ef.rsp
Normal file
4
ef.rsp
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
--project
|
||||||
|
src/Kyoo.Postgresql
|
||||||
|
--msbuildprojectextensionspath
|
||||||
|
out/obj/Kyoo.Postgresql
|
||||||
14
nuget.config
Normal file
14
nuget.config
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<clear/>
|
||||||
|
<add key="feedz" value="https://f.feedz.io/zoriya/entityframeworkcore-projectables/nuget/index.json"/>
|
||||||
|
<add key="nuget" value="https://api.nuget.org/v3/index.json"/>
|
||||||
|
</packageSources>
|
||||||
|
<disabledPackageSources>
|
||||||
|
<clear/>
|
||||||
|
</disabledPackageSources>
|
||||||
|
<fallbackPackageFolders>
|
||||||
|
<clear/>
|
||||||
|
</fallbackPackageFolders>
|
||||||
|
</configuration>
|
||||||
15
shell.nix
Normal file
15
shell.nix
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{pkgs ? import <nixpkgs> {}}: let
|
||||||
|
dotnet = with pkgs.dotnetCorePackages;
|
||||||
|
combinePackages [
|
||||||
|
sdk_8_0
|
||||||
|
aspnetcore_8_0
|
||||||
|
];
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
packages = with pkgs; [
|
||||||
|
dotnet
|
||||||
|
csharpier
|
||||||
|
];
|
||||||
|
|
||||||
|
DOTNET_ROOT = "${dotnet}";
|
||||||
|
}
|
||||||
47
src/Directory.Build.props
Normal file
47
src/Directory.Build.props
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<LangVersion>default</LangVersion>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<Company>Kyoo</Company>
|
||||||
|
<Authors>Kyoo</Authors>
|
||||||
|
<Copyright>Copyright (c) Kyoo</Copyright>
|
||||||
|
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||||
|
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
|
||||||
|
<RequireLicenseAcceptance>true</RequireLicenseAcceptance>
|
||||||
|
|
||||||
|
<RepositoryUrl>https://github.com/zoriya/Kyoo</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<PublishRepositoryUrl>true</PublishRepositoryUrl>
|
||||||
|
<PackageProjectUrl>https://github.com/zoriya/Kyoo</PackageProjectUrl>
|
||||||
|
|
||||||
|
<PackageVersion>1.0.0</PackageVersion>
|
||||||
|
<IncludeSymbols>true</IncludeSymbols>
|
||||||
|
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
|
||||||
|
|
||||||
|
<ApplicationIcon>$(MSBuildThisFileDirectory)../icon.ico</ApplicationIcon>
|
||||||
|
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<CheckCodingStyle Condition="$(CheckCodingStyle) == ''">true</CheckCodingStyle>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<BaseIntermediateOutputPath>$(MsBuildThisFileDirectory)/../out/obj/$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||||
|
<BaseOutputPath>$(MsBuildThisFileDirectory)/../out/bin/$(MSBuildProjectName)</BaseOutputPath>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup Condition="$(CheckCodingStyle) == true">
|
||||||
|
<None Include="$(MSBuildThisFileDirectory)../.editorconfig" Link=".editorconfig" Visible="false" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup Condition="$(CheckCodingStyle) == true">
|
||||||
|
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)../Kyoo.ruleset</CodeAnalysisRuleSet>
|
||||||
|
<NoWarn>1591;1305;8618;SYSLIB1045;CS1573</NoWarn>
|
||||||
|
<!-- <AnalysisMode>All</AnalysisMode> -->
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- vim: ft=xml -->
|
||||||
|
</Project>
|
||||||
232
src/Kyoo.Abstractions/.gitignore
vendored
Normal file
232
src/Kyoo.Abstractions/.gitignore
vendored
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
## PROJECT CUSTOM IGNORES
|
||||||
|
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
build/
|
||||||
|
bld/
|
||||||
|
bin/
|
||||||
|
Bin/
|
||||||
|
obj/
|
||||||
|
Obj/
|
||||||
|
|
||||||
|
# Visual Studio 2015 cache/options directory
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/packages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/packages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/packages/repositories.config
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Microsoft Azure ApplicationInsights config file
|
||||||
|
ApplicationInsights.config
|
||||||
|
|
||||||
|
# Windows Store app package directory
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
35
src/Kyoo.Abstractions/Controllers/IIssueRepository.cs
Normal file
35
src/Kyoo.Abstractions/Controllers/IIssueRepository.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
public interface IIssueRepository
|
||||||
|
{
|
||||||
|
Task<ICollection<Issue>> GetAll(Filter<Issue>? filter = default);
|
||||||
|
|
||||||
|
Task<int> GetCount(Filter<Issue>? filter = default);
|
||||||
|
|
||||||
|
Task<Issue> Upsert(Issue issue);
|
||||||
|
|
||||||
|
Task DeleteAll(Filter<Issue>? filter = default);
|
||||||
|
}
|
||||||
80
src/Kyoo.Abstractions/Controllers/ILibraryManager.cs
Normal file
80
src/Kyoo.Abstractions/Controllers/ILibraryManager.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interface to interact with the database. Every repository is mapped through here.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILibraryManager
|
||||||
|
{
|
||||||
|
IRepository<T> Repository<T>()
|
||||||
|
where T : IResource, IQuery;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle libraries items (a wrapper around shows and collections).
|
||||||
|
/// </summary>
|
||||||
|
IRepository<ILibraryItem> LibraryItems { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle new items.
|
||||||
|
/// </summary>
|
||||||
|
IRepository<INews> News { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle watched items.
|
||||||
|
/// </summary>
|
||||||
|
IWatchStatusRepository WatchStatus { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle collections.
|
||||||
|
/// </summary>
|
||||||
|
IRepository<Collection> Collections { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle shows.
|
||||||
|
/// </summary>
|
||||||
|
IRepository<Movie> Movies { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle shows.
|
||||||
|
/// </summary>
|
||||||
|
IRepository<Show> Shows { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle seasons.
|
||||||
|
/// </summary>
|
||||||
|
IRepository<Season> Seasons { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle episodes.
|
||||||
|
/// </summary>
|
||||||
|
IRepository<Episode> Episodes { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle studios.
|
||||||
|
/// </summary>
|
||||||
|
IRepository<Studio> Studios { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The repository that handle users.
|
||||||
|
/// </summary>
|
||||||
|
IRepository<User> Users { get; }
|
||||||
|
}
|
||||||
46
src/Kyoo.Abstractions/Controllers/IPermissionValidator.cs
Normal file
46
src/Kyoo.Abstractions/Controllers/IPermissionValidator.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Models.Permissions;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A service to validate permissions.
|
||||||
|
/// </summary>
|
||||||
|
public interface IPermissionValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create an IAuthorizationFilter that will be used to validate permissions.
|
||||||
|
/// This can registered with any lifetime.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="attribute">The permission attribute to validate.</param>
|
||||||
|
/// <returns>An authorization filter used to validate the permission.</returns>
|
||||||
|
IFilterMetadata Create(PermissionAttribute attribute);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create an IAuthorizationFilter that will be used to validate permissions.
|
||||||
|
/// This can registered with any lifetime.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="attribute">
|
||||||
|
/// A partial attribute to validate. See <see cref="PartialPermissionAttribute"/>.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>An authorization filter used to validate the permission.</returns>
|
||||||
|
IFilterMetadata Create(PartialPermissionAttribute attribute);
|
||||||
|
}
|
||||||
267
src/Kyoo.Abstractions/Controllers/IRepository.cs
Normal file
267
src/Kyoo.Abstractions/Controllers/IRepository.cs
Normal file
@@ -0,0 +1,267 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A common repository for every resources.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The resource's type that this repository manage.</typeparam>
|
||||||
|
public interface IRepository<T> : IBaseRepository
|
||||||
|
where T : IResource, IQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The event handler type for all events of this repository.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resource">The resource created/modified/deleted</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
public delegate Task ResourceEventHandler(T resource);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a resource from it's ID.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The id of the resource</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||||
|
/// <returns>The resource found</returns>
|
||||||
|
Task<T> Get(Guid id, Include<T>? include = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a resource from it's slug.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slug">The slug of the resource</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||||
|
/// <returns>The resource found</returns>
|
||||||
|
Task<T> Get(string slug, Include<T>? include = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the first resource that match the predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">A predicate to filter the resource.</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
|
||||||
|
/// <param name="reverse">Reverse the sort.</param>
|
||||||
|
/// <param name="afterId">Select the first element after this id if it was in a list.</param>
|
||||||
|
/// <exception cref="ItemNotFoundException">If the item could not be found.</exception>
|
||||||
|
/// <returns>The resource found</returns>
|
||||||
|
Task<T> Get(
|
||||||
|
Filter<T> filter,
|
||||||
|
Include<T>? include = default,
|
||||||
|
Sort<T>? sortBy = default,
|
||||||
|
bool reverse = false,
|
||||||
|
Guid? afterId = default
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a resource from it's ID or null if it is not found.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The id of the resource</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>The resource found</returns>
|
||||||
|
Task<T?> GetOrDefault(Guid id, Include<T>? include = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get a resource from it's slug or null if it is not found.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slug">The slug of the resource</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>The resource found</returns>
|
||||||
|
Task<T?> GetOrDefault(string slug, Include<T>? include = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the first resource that match the predicate or null if it is not found.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">A predicate to filter the resource.</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <param name="sortBy">A custom sort method to handle cases where multiples items match the filters.</param>
|
||||||
|
/// <param name="reverse">Reverse the sort.</param>
|
||||||
|
/// <param name="afterId">Select the first element after this id if it was in a list.</param>
|
||||||
|
/// <returns>The resource found</returns>
|
||||||
|
Task<T?> GetOrDefault(
|
||||||
|
Filter<T>? filter,
|
||||||
|
Include<T>? include = default,
|
||||||
|
Sort<T>? sortBy = default,
|
||||||
|
bool reverse = false,
|
||||||
|
Guid? afterId = default
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for resources with the database.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The query string.</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>A list of resources found</returns>
|
||||||
|
Task<ICollection<T>> Search(string query, Include<T>? include = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get every resources that match all filters
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">A filter predicate</param>
|
||||||
|
/// <param name="sort">Sort information about the query (sort by, sort order)</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <param name="limit">How pagination should be done (where to start and how many to return)</param>
|
||||||
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
Task<ICollection<T>> GetAll(
|
||||||
|
Filter<T>? filter = null,
|
||||||
|
Sort<T>? sort = default,
|
||||||
|
Include<T>? include = default,
|
||||||
|
Pagination? limit = default
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the number of resources that match the filter's predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">A filter predicate</param>
|
||||||
|
/// <returns>How many resources matched that filter</returns>
|
||||||
|
Task<int> GetCount(Filter<T>? filter = null);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Map a list of ids to a list of items (keep the order).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ids">The list of items id.</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>A list of resources mapped from ids.</returns>
|
||||||
|
Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new resource.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The item to register</param>
|
||||||
|
/// <returns>The resource registers and completed by database's information (related items and so on)</returns>
|
||||||
|
Task<T> Create(T obj);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new resource if it does not exist already. If it does, the existing value is returned instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The object to create</param>
|
||||||
|
/// <returns>The newly created item or the existing value if it existed.</returns>
|
||||||
|
Task<T> CreateIfNotExists(T obj);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a resource has been created.
|
||||||
|
/// </summary>
|
||||||
|
static event ResourceEventHandler OnCreated;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback that should be called after a resource has been created.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The resource newly created.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
protected static Task OnResourceCreated(T obj) => OnCreated?.Invoke(obj) ?? Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Edit a resource and replace every property
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="edited">The resource to edit, it's ID can't change.</param>
|
||||||
|
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||||
|
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
|
||||||
|
Task<T> Edit(T edited);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Edit only specific properties of a resource
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The id of the resource to edit</param>
|
||||||
|
/// <param name="patch">
|
||||||
|
/// A method that will be called when you need to update every properties that you want to
|
||||||
|
/// persist.
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||||
|
/// <returns>The resource edited and completed by database's information (related items and so on)</returns>
|
||||||
|
Task<T> Patch(Guid id, Func<T, T> patch);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a resource has been edited.
|
||||||
|
/// </summary>
|
||||||
|
static event ResourceEventHandler OnEdited;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback that should be called after a resource has been edited.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The resource newly edited.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
protected static Task OnResourceEdited(T obj) => OnEdited?.Invoke(obj) ?? Task.CompletedTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a resource by it's ID
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The ID of the resource</param>
|
||||||
|
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
Task Delete(Guid id);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a resource by it's slug
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slug">The slug of the resource</param>
|
||||||
|
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
Task Delete(string slug);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete a resource
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The resource to delete</param>
|
||||||
|
/// <exception cref="ItemNotFoundException">If the item is not found</exception>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
Task Delete(T obj);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete all resources that match the predicate.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="filter">A predicate to filter resources to delete. Every resource that match this will be deleted.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
Task DeleteAll(Filter<T> filter);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called when a resource has been edited.
|
||||||
|
/// </summary>
|
||||||
|
static event ResourceEventHandler OnDeleted;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Callback that should be called after a resource has been deleted.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="obj">The resource newly deleted.</param>
|
||||||
|
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
|
||||||
|
protected static Task OnResourceDeleted(T obj) => OnDeleted?.Invoke(obj) ?? Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A base class for repositories. Every service implementing this will be handled by the <see cref="ILibraryManager"/>.
|
||||||
|
/// </summary>
|
||||||
|
public interface IBaseRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type for witch this repository is responsible or null if non applicable.
|
||||||
|
/// </summary>
|
||||||
|
Type RepositoryType { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IUserRepository : IRepository<User>
|
||||||
|
{
|
||||||
|
Task<User?> GetByExternalId(string provider, string id);
|
||||||
|
Task<User> AddExternalToken(Guid userId, string provider, ExternalToken token);
|
||||||
|
Task<User> DeleteExternalToken(Guid userId, string provider);
|
||||||
|
}
|
||||||
28
src/Kyoo.Abstractions/Controllers/IScanner.cs
Normal file
28
src/Kyoo.Abstractions/Controllers/IScanner.cs
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
public interface IScanner
|
||||||
|
{
|
||||||
|
Task SendRescanRequest();
|
||||||
|
Task SendRefreshRequest(string kind, Guid id);
|
||||||
|
}
|
||||||
125
src/Kyoo.Abstractions/Controllers/ISearchManager.cs
Normal file
125
src/Kyoo.Abstractions/Controllers/ISearchManager.cs
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The service to search items.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISearchManager
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Search for items.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The seach query.</param>
|
||||||
|
/// <param name="sortBy">Sort information about the query (sort by, sort order)</param>
|
||||||
|
/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
public Task<SearchPage<ILibraryItem>.SearchResult> SearchItems(
|
||||||
|
string? query,
|
||||||
|
Sort<ILibraryItem> sortBy,
|
||||||
|
Filter<ILibraryItem>? filter,
|
||||||
|
SearchPagination pagination,
|
||||||
|
Include<ILibraryItem>? include = default
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for movies.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The seach query.</param>
|
||||||
|
/// <param name="sortBy">Sort information about the query (sort by, sort order)</param>
|
||||||
|
/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
public Task<SearchPage<Movie>.SearchResult> SearchMovies(
|
||||||
|
string? query,
|
||||||
|
Sort<Movie> sortBy,
|
||||||
|
Filter<ILibraryItem>? filter,
|
||||||
|
SearchPagination pagination,
|
||||||
|
Include<Movie>? include = default
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for shows.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The seach query.</param>
|
||||||
|
/// <param name="sortBy">Sort information about the query (sort by, sort order)</param>
|
||||||
|
/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
public Task<SearchPage<Show>.SearchResult> SearchShows(
|
||||||
|
string? query,
|
||||||
|
Sort<Show> sortBy,
|
||||||
|
Filter<ILibraryItem>? filter,
|
||||||
|
SearchPagination pagination,
|
||||||
|
Include<Show>? include = default
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for collections.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The seach query.</param>
|
||||||
|
/// <param name="sortBy">Sort information about the query (sort by, sort order)</param>
|
||||||
|
/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
public Task<SearchPage<Collection>.SearchResult> SearchCollections(
|
||||||
|
string? query,
|
||||||
|
Sort<Collection> sortBy,
|
||||||
|
Filter<ILibraryItem>? filter,
|
||||||
|
SearchPagination pagination,
|
||||||
|
Include<Collection>? include = default
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for episodes.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The seach query.</param>
|
||||||
|
/// <param name="sortBy">Sort information about the query (sort by, sort order)</param>
|
||||||
|
/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
public Task<SearchPage<Episode>.SearchResult> SearchEpisodes(
|
||||||
|
string? query,
|
||||||
|
Sort<Episode> sortBy,
|
||||||
|
Filter<Episode>? filter,
|
||||||
|
SearchPagination pagination,
|
||||||
|
Include<Episode>? include = default
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search for studios.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The seach query.</param>
|
||||||
|
/// <param name="sortBy">Sort information about the query (sort by, sort order)</param>
|
||||||
|
/// <param name="pagination">How pagination should be done (where to start and how many to return)</param>
|
||||||
|
/// <param name="include">The related fields to include.</param>
|
||||||
|
/// <returns>A list of resources that match every filters</returns>
|
||||||
|
public Task<SearchPage<Studio>.SearchResult> SearchStudios(
|
||||||
|
string? query,
|
||||||
|
Sort<Studio> sortBy,
|
||||||
|
Filter<Studio>? filter,
|
||||||
|
SearchPagination pagination,
|
||||||
|
Include<Studio>? include = default
|
||||||
|
);
|
||||||
|
}
|
||||||
43
src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs
Normal file
43
src/Kyoo.Abstractions/Controllers/IThumbnailsManager.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
public interface IThumbnailsManager
|
||||||
|
{
|
||||||
|
Task DownloadImages<T>(T item)
|
||||||
|
where T : IThumbnails;
|
||||||
|
|
||||||
|
Task DownloadImage(Image? image, string what);
|
||||||
|
|
||||||
|
Task<bool> IsImageSaved(Guid imageId, ImageQuality quality);
|
||||||
|
|
||||||
|
Task<Stream> GetImage(Guid imageId, ImageQuality quality);
|
||||||
|
|
||||||
|
Task DeleteImages<T>(T item)
|
||||||
|
where T : IThumbnails;
|
||||||
|
|
||||||
|
Task<Stream> GetUserImage(Guid userId);
|
||||||
|
|
||||||
|
Task SetUserImage(Guid userId, Stream? image);
|
||||||
|
}
|
||||||
83
src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs
Normal file
83
src/Kyoo.Abstractions/Controllers/IWatchStatusRepository.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A local repository to handle watched items
|
||||||
|
/// </summary>
|
||||||
|
public interface IWatchStatusRepository
|
||||||
|
{
|
||||||
|
public delegate Task ResourceEventHandler<T>(T resource);
|
||||||
|
|
||||||
|
Task<ICollection<IWatchlist>> GetAll(
|
||||||
|
Filter<IWatchlist>? filter = default,
|
||||||
|
Include<IWatchlist>? include = default,
|
||||||
|
Pagination? limit = default
|
||||||
|
);
|
||||||
|
|
||||||
|
Task<MovieWatchStatus?> GetMovieStatus(Guid movieId, Guid userId);
|
||||||
|
|
||||||
|
Task<MovieWatchStatus?> SetMovieStatus(
|
||||||
|
Guid movieId,
|
||||||
|
Guid userId,
|
||||||
|
WatchStatus status,
|
||||||
|
int? watchedTime,
|
||||||
|
int? percent
|
||||||
|
);
|
||||||
|
|
||||||
|
static event ResourceEventHandler<WatchStatus<Movie>> OnMovieStatusChangedHandler;
|
||||||
|
protected static Task OnMovieStatusChanged(WatchStatus<Movie> obj) =>
|
||||||
|
OnMovieStatusChangedHandler?.Invoke(obj) ?? Task.CompletedTask;
|
||||||
|
|
||||||
|
Task DeleteMovieStatus(Guid movieId, Guid userId);
|
||||||
|
|
||||||
|
Task<ShowWatchStatus?> GetShowStatus(Guid showId, Guid userId);
|
||||||
|
|
||||||
|
Task<ShowWatchStatus?> SetShowStatus(Guid showId, Guid userId, WatchStatus status);
|
||||||
|
|
||||||
|
static event ResourceEventHandler<WatchStatus<Show>> OnShowStatusChangedHandler;
|
||||||
|
protected static Task OnShowStatusChanged(WatchStatus<Show> obj) =>
|
||||||
|
OnShowStatusChangedHandler?.Invoke(obj) ?? Task.CompletedTask;
|
||||||
|
|
||||||
|
Task DeleteShowStatus(Guid showId, Guid userId);
|
||||||
|
|
||||||
|
Task<EpisodeWatchStatus?> GetEpisodeStatus(Guid episodeId, Guid userId);
|
||||||
|
|
||||||
|
/// <param name="watchedTime">Where the user has stopped watching. Only usable if Status
|
||||||
|
/// is <see cref="WatchStatus.Watching"/></param>
|
||||||
|
Task<EpisodeWatchStatus?> SetEpisodeStatus(
|
||||||
|
Guid episodeId,
|
||||||
|
Guid userId,
|
||||||
|
WatchStatus status,
|
||||||
|
int? watchedTime,
|
||||||
|
int? percent
|
||||||
|
);
|
||||||
|
|
||||||
|
static event ResourceEventHandler<WatchStatus<Episode>> OnEpisodeStatusChangedHandler;
|
||||||
|
protected static Task OnEpisodeStatusChanged(WatchStatus<Episode> obj) =>
|
||||||
|
OnEpisodeStatusChangedHandler?.Invoke(obj) ?? Task.CompletedTask;
|
||||||
|
|
||||||
|
Task DeleteEpisodeStatus(Guid episodeId, Guid userId);
|
||||||
|
}
|
||||||
64
src/Kyoo.Abstractions/Extensions.cs
Normal file
64
src/Kyoo.Abstractions/Extensions.cs
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
|
using Kyoo.Authentication.Models;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods.
|
||||||
|
/// </summary>
|
||||||
|
public static class Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Get the permissions of an user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user</param>
|
||||||
|
/// <returns>The list of permissions</returns>
|
||||||
|
public static ICollection<string> GetPermissions(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
return user.Claims.FirstOrDefault(x => x.Type == Claims.Permissions)?.Value.Split(',')
|
||||||
|
?? Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the id of the current user or null if unlogged or invalid.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user.</param>
|
||||||
|
/// <returns>The id of the user or null.</returns>
|
||||||
|
public static Guid? GetId(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
Claim? value = user.FindFirst(Claims.Id);
|
||||||
|
if (Guid.TryParse(value?.Value, out Guid id))
|
||||||
|
return id;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Guid GetIdOrThrow(this ClaimsPrincipal user)
|
||||||
|
{
|
||||||
|
Guid? ret = user.GetId();
|
||||||
|
if (ret == null)
|
||||||
|
throw new UnauthorizedException();
|
||||||
|
return ret.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
17
src/Kyoo.Abstractions/Kyoo.Abstractions.csproj
Normal file
17
src/Kyoo.Abstractions/Kyoo.Abstractions.csproj
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<Title>Kyoo.Abstractions</Title>
|
||||||
|
<Description>Base package to create plugins for Kyoo.</Description>
|
||||||
|
<RootNamespace>Kyoo.Abstractions</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||||
|
<PackageReference Include="EntityFrameworkCore.Projectables" Version="4.1.4-prebeta" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||||
|
<PackageReference Include="Sprache" Version="2.3.1" />
|
||||||
|
<PackageReference Include="System.ComponentModel.Composition" Version="9.0.10" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute to specify on apis to specify it's documentation's name and category.
|
||||||
|
/// If this is applied on a method, the specified method will be exploded from the controller's page and be
|
||||||
|
/// included on the specified tag page.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||||
|
public class ApiDefinitionAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The public name of this api.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the group in witch this API is. You can also specify a custom sort order using the following
|
||||||
|
/// format: <code>order:name</code>. Everything before the first <c>:</c> will be removed but kept for
|
||||||
|
/// th alphabetical ordering.
|
||||||
|
/// </summary>
|
||||||
|
public string? Group { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ApiDefinitionAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the api that will be used on the documentation page.</param>
|
||||||
|
public ApiDefinitionAttribute(string name)
|
||||||
|
{
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/Kyoo.Abstractions/Models/Attributes/ComputedAttribute.cs
Normal file
27
src/Kyoo.Abstractions/Models/Attributes/ComputedAttribute.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute to inform that the property is computed automatically and can't be assigned manually.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class ComputedAttribute : NotMergeableAttribute { }
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The targeted relation can be loaded.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class LoadableRelationAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the field containing the related resource's ID.
|
||||||
|
/// </summary>
|
||||||
|
public string? RelationID { get; }
|
||||||
|
|
||||||
|
public string? Sql { get; set; }
|
||||||
|
|
||||||
|
public string? On { get; set; }
|
||||||
|
|
||||||
|
public string? Projected { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="LoadableRelationAttribute"/>.
|
||||||
|
/// </summary>
|
||||||
|
public LoadableRelationAttribute() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="LoadableRelationAttribute"/> with a baking relationID field.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="relationID">The name of the RelationID field.</param>
|
||||||
|
public LoadableRelationAttribute(string relationID)
|
||||||
|
{
|
||||||
|
RelationID = relationID;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify that a property can't be merged.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Property)]
|
||||||
|
public class NotMergeableAttribute : Attribute { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interface with a method called when this object is merged.
|
||||||
|
/// </summary>
|
||||||
|
public interface IOnMerge
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This function is called after the object has been merged.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="merged">The object that has been merged with this.</param>
|
||||||
|
void OnMerge(object merged);
|
||||||
|
}
|
||||||
33
src/Kyoo.Abstractions/Models/Attributes/OneOfAttribute.cs
Normal file
33
src/Kyoo.Abstractions/Models/Attributes/OneOfAttribute.cs
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An attribute to inform that this interface is a type union
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Interface)]
|
||||||
|
public class OneOfAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The types this union concist of.
|
||||||
|
/// </summary>
|
||||||
|
public Type[] Types { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Permissions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify one part of a permissions needed for the API (the kind or the type).
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public class PartialPermissionAttribute : Attribute, IFilterFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The needed permission type.
|
||||||
|
/// </summary>
|
||||||
|
public string? Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The needed permission kind.
|
||||||
|
/// </summary>
|
||||||
|
public Kind? Kind { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The group of this permission.
|
||||||
|
/// </summary>
|
||||||
|
public Group Group { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ask a permission to run an action.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// With this attribute, you can only specify a type or a kind.
|
||||||
|
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
|
||||||
|
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
|
||||||
|
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
|
||||||
|
/// lead to unspecified behaviors.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="type">The type of the action</param>
|
||||||
|
public PartialPermissionAttribute(string type)
|
||||||
|
{
|
||||||
|
Type = type.ToLower();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ask a permission to run an action.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// With this attribute, you can only specify a type or a kind.
|
||||||
|
/// To have a valid permission attribute, you must specify the kind and the permission using two attributes.
|
||||||
|
/// Those attributes can be dispatched at different places (one on the class, one on the method for example).
|
||||||
|
/// If you don't put exactly two of those attributes, the permission attribute will be ill-formed and will
|
||||||
|
/// lead to unspecified behaviors.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="permission">The kind of permission needed.</param>
|
||||||
|
public PartialPermissionAttribute(Kind permission)
|
||||||
|
{
|
||||||
|
Kind = permission;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsReusable => true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Permissions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The kind of permission needed.
|
||||||
|
/// </summary>
|
||||||
|
public enum Kind
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Allow the user to read for this kind of data.
|
||||||
|
/// </summary>
|
||||||
|
Read,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allow the user to write for this kind of data.
|
||||||
|
/// </summary>
|
||||||
|
Write,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allow the user to create this kind of data.
|
||||||
|
/// </summary>
|
||||||
|
Create,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allow the user to delete this kind of data.
|
||||||
|
/// </summary>
|
||||||
|
Delete,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allow the user to play this file.
|
||||||
|
/// </summary>
|
||||||
|
Play,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The group of the permission.
|
||||||
|
/// </summary>
|
||||||
|
public enum Group
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Default group indicating no value.
|
||||||
|
/// </summary>
|
||||||
|
None,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allow all operations on basic items types.
|
||||||
|
/// </summary>
|
||||||
|
Overall,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allow operation on sensitive items like libraries path, configurations and so on.
|
||||||
|
/// </summary>
|
||||||
|
Admin
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specify permissions needed for the API.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public class PermissionAttribute : Attribute, IFilterFactory
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The needed permission as string.
|
||||||
|
/// </summary>
|
||||||
|
public string Type { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The needed permission kind.
|
||||||
|
/// </summary>
|
||||||
|
public Kind Kind { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The group of this permission.
|
||||||
|
/// </summary>
|
||||||
|
public Group Group { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ask a permission to run an action.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">
|
||||||
|
/// The type of the action
|
||||||
|
/// </param>
|
||||||
|
/// <param name="permission">
|
||||||
|
/// The kind of permission needed.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="group">
|
||||||
|
/// The group of this permission (allow grouped permission like overall.read
|
||||||
|
/// for all read permissions of this group).
|
||||||
|
/// </param>
|
||||||
|
public PermissionAttribute(string type, Kind permission, Group group = Group.Overall)
|
||||||
|
{
|
||||||
|
Type = type.ToLower();
|
||||||
|
Kind = permission;
|
||||||
|
Group = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IFilterMetadata CreateInstance(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
return serviceProvider.GetRequiredService<IPermissionValidator>().Create(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsReusable => true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return this permission attribute as a string.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The string representation.</returns>
|
||||||
|
public string AsPermissionString()
|
||||||
|
{
|
||||||
|
return Type;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Permissions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The annotated route can only be accessed by a logged in user.
|
||||||
|
/// </summary>
|
||||||
|
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
|
||||||
|
public class UserOnlyAttribute : Attribute
|
||||||
|
{
|
||||||
|
// TODO: Implement a Filter Attribute to make this work. For now, this attribute is only useful as documentation.
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class SqlFirstColumnAttribute : Attribute
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the first column of the element. Used to split multiples
|
||||||
|
/// items on a single sql query. If not specified, it defaults to "Id".
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
public SqlFirstColumnAttribute(string name)
|
||||||
|
{
|
||||||
|
Name = name.ToSnakeCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An exception raised when an item already exists in the database.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class DuplicatedItemException(object? existing = null)
|
||||||
|
: Exception("Already exists in the database.")
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The existing object.
|
||||||
|
/// </summary>
|
||||||
|
public object? Existing { get; } = existing;
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Exceptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An exception raised when an item could not be found.
|
||||||
|
/// </summary>
|
||||||
|
[Serializable]
|
||||||
|
public class ItemNotFoundException : Exception
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a default <see cref="ItemNotFoundException"/> with no message.
|
||||||
|
/// </summary>
|
||||||
|
public ItemNotFoundException()
|
||||||
|
: base("Item not found") { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="ItemNotFoundException"/> with a message
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="message">The message of the exception</param>
|
||||||
|
public ItemNotFoundException(string message)
|
||||||
|
: base(message) { }
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Exceptions;
|
||||||
|
|
||||||
|
[Serializable]
|
||||||
|
public class UnauthorizedException : Exception
|
||||||
|
{
|
||||||
|
public UnauthorizedException()
|
||||||
|
: base("User not authenticated or token invalid.") { }
|
||||||
|
|
||||||
|
public UnauthorizedException(string message)
|
||||||
|
: base(message) { }
|
||||||
|
}
|
||||||
50
src/Kyoo.Abstractions/Models/Genre.cs
Normal file
50
src/Kyoo.Abstractions/Models/Genre.cs
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A genre that allow one to specify categories for shows.
|
||||||
|
/// </summary>
|
||||||
|
public enum Genre
|
||||||
|
{
|
||||||
|
Action,
|
||||||
|
Adventure,
|
||||||
|
Animation,
|
||||||
|
Comedy,
|
||||||
|
Crime,
|
||||||
|
Documentary,
|
||||||
|
Drama,
|
||||||
|
Family,
|
||||||
|
Fantasy,
|
||||||
|
History,
|
||||||
|
Horror,
|
||||||
|
Music,
|
||||||
|
Mystery,
|
||||||
|
Romance,
|
||||||
|
ScienceFiction,
|
||||||
|
Thriller,
|
||||||
|
War,
|
||||||
|
Western,
|
||||||
|
Kids,
|
||||||
|
News,
|
||||||
|
Reality,
|
||||||
|
Soap,
|
||||||
|
Talk,
|
||||||
|
Politics,
|
||||||
|
}
|
||||||
31
src/Kyoo.Abstractions/Models/ILibraryItem.cs
Normal file
31
src/Kyoo.Abstractions/Models/ILibraryItem.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A show, a movie or a collection.
|
||||||
|
/// </summary>
|
||||||
|
[OneOf(Types = new[] { typeof(Show), typeof(Movie), typeof(Collection) })]
|
||||||
|
public interface ILibraryItem : IResource, IThumbnails, IMetadata, IAddedDate, IQuery
|
||||||
|
{
|
||||||
|
static Sort IQuery.DefaultSort => new Sort<ILibraryItem>.By(nameof(Movie.Name));
|
||||||
|
}
|
||||||
31
src/Kyoo.Abstractions/Models/INews.cs
Normal file
31
src/Kyoo.Abstractions/Models/INews.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A show, a movie or a collection.
|
||||||
|
/// </summary>
|
||||||
|
[OneOf(Types = [typeof(Episode), typeof(Movie)])]
|
||||||
|
public interface INews : IResource, IThumbnails, IAddedDate, IQuery
|
||||||
|
{
|
||||||
|
static Sort IQuery.DefaultSort => new Sort<INews>.By(nameof(AddedDate), true);
|
||||||
|
}
|
||||||
27
src/Kyoo.Abstractions/Models/IWatchlist.cs
Normal file
27
src/Kyoo.Abstractions/Models/IWatchlist.cs
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A watch list item.
|
||||||
|
/// </summary>
|
||||||
|
[OneOf(Types = new[] { typeof(Show), typeof(Movie) })]
|
||||||
|
public interface IWatchlist : IResource, IThumbnails, IMetadata, IAddedDate { }
|
||||||
52
src/Kyoo.Abstractions/Models/Issues.cs
Normal file
52
src/Kyoo.Abstractions/Models/Issues.cs
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An issue that occured on kyoo.
|
||||||
|
/// </summary>
|
||||||
|
public class Issue : IAddedDate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type of issue (for example, "Scanner" if this issue was created due to scanning error).
|
||||||
|
/// </summary>
|
||||||
|
public string Domain { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Why this issue was caused? An unique cause that can be used to identify this issue.
|
||||||
|
/// For the scanner, a cause should be a video path.
|
||||||
|
/// </summary>
|
||||||
|
public string Cause { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A human readable string explaining why this issue occured.
|
||||||
|
/// </summary>
|
||||||
|
public string Reason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Some extra data that could store domain-specific info.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, object> Extra { get; set; } = new();
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
}
|
||||||
61
src/Kyoo.Abstractions/Models/MetadataID.cs
Normal file
61
src/Kyoo.Abstractions/Models/MetadataID.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ID and link of an item on an external provider.
|
||||||
|
/// </summary>
|
||||||
|
public class MetadataId
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the resource on the external provider.
|
||||||
|
/// </summary>
|
||||||
|
public string DataId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL of the resource on the external provider.
|
||||||
|
/// </summary>
|
||||||
|
public string? Link { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// ID informations about an episode.
|
||||||
|
/// </summary>
|
||||||
|
public class EpisodeId
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The Id of the show on the metadata database.
|
||||||
|
/// </summary>
|
||||||
|
public string ShowId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The season number or null if absolute numbering is used in this database.
|
||||||
|
/// </summary>
|
||||||
|
public int? Season { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The episode number or absolute number if Season is null.
|
||||||
|
/// </summary>
|
||||||
|
public int Episode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The URL of the resource on the external provider.
|
||||||
|
/// </summary>
|
||||||
|
public string? Link { get; set; }
|
||||||
|
}
|
||||||
105
src/Kyoo.Abstractions/Models/Page.cs
Normal file
105
src/Kyoo.Abstractions/Models/Page.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A page of resource that contains information about the pagination of resources.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of resource contained in this page.</typeparam>
|
||||||
|
public class Page<T>
|
||||||
|
where T : IResource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The link of the current page.
|
||||||
|
/// </summary>
|
||||||
|
public string This { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The link of the first page.
|
||||||
|
/// </summary>
|
||||||
|
public string First { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The link of the previous page.
|
||||||
|
/// </summary>
|
||||||
|
public string? Previous { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The link of the next page.
|
||||||
|
/// </summary>
|
||||||
|
public string? Next { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of items in the current page.
|
||||||
|
/// </summary>
|
||||||
|
public int Count => Items.Count;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of items in the page.
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<T> Items { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Page{T}"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">The list of items in the page.</param>
|
||||||
|
/// <param name="this">The link of the current page.</param>
|
||||||
|
/// <param name="previous">The link of the previous page.</param>
|
||||||
|
/// <param name="next">The link of the next page.</param>
|
||||||
|
/// <param name="first">The link of the first page.</param>
|
||||||
|
public Page(ICollection<T> items, string @this, string? previous, string? next, string first)
|
||||||
|
{
|
||||||
|
Items = items;
|
||||||
|
This = @this;
|
||||||
|
Previous = previous;
|
||||||
|
Next = next;
|
||||||
|
First = first;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Page{T}"/> and compute the urls.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="items">The list of items in the page.</param>
|
||||||
|
/// <param name="url">The base url of the resources available from this page.</param>
|
||||||
|
/// <param name="query">The list of query strings of the current page</param>
|
||||||
|
/// <param name="limit">The number of items requested for the current page.</param>
|
||||||
|
public Page(ICollection<T> items, string url, Dictionary<string, string> query, int limit)
|
||||||
|
{
|
||||||
|
Items = items;
|
||||||
|
This = url + query.ToQueryString();
|
||||||
|
if (items.Count > 0 && query.ContainsKey("afterID"))
|
||||||
|
{
|
||||||
|
query["afterID"] = items.First().Id.ToString();
|
||||||
|
query["reverse"] = "true";
|
||||||
|
Previous = url + query.ToQueryString();
|
||||||
|
}
|
||||||
|
query.Remove("reverse");
|
||||||
|
if (items.Count == limit && limit > 0)
|
||||||
|
{
|
||||||
|
query["afterID"] = items.Last().Id.ToString();
|
||||||
|
Next = url + query.ToQueryString();
|
||||||
|
}
|
||||||
|
query.Remove("afterID");
|
||||||
|
First = url + query.ToQueryString();
|
||||||
|
}
|
||||||
|
}
|
||||||
46
src/Kyoo.Abstractions/Models/Patch.cs
Normal file
46
src/Kyoo.Abstractions/Models/Patch.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
namespace Kyoo.Models;
|
||||||
|
|
||||||
|
public class Patch<T> : Dictionary<string, JsonDocument>
|
||||||
|
where T : class, IResource
|
||||||
|
{
|
||||||
|
public Guid? Id => this.GetValueOrDefault(nameof(IResource.Id))?.Deserialize<Guid>();
|
||||||
|
|
||||||
|
public string? Slug => this.GetValueOrDefault(nameof(IResource.Slug))?.Deserialize<string>();
|
||||||
|
|
||||||
|
public T Apply(T current)
|
||||||
|
{
|
||||||
|
foreach ((string property, JsonDocument value) in this)
|
||||||
|
{
|
||||||
|
PropertyInfo prop = typeof(T).GetProperty(
|
||||||
|
property,
|
||||||
|
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||||
|
)!;
|
||||||
|
prop.SetValue(current, value.Deserialize(prop.PropertyType));
|
||||||
|
}
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
src/Kyoo.Abstractions/Models/Resources/Collection.cs
Normal file
100
src/Kyoo.Abstractions/Models/Resources/Collection.cs
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class representing collections of <see cref="Show"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class Collection
|
||||||
|
: IQuery,
|
||||||
|
IResource,
|
||||||
|
IMetadata,
|
||||||
|
IThumbnails,
|
||||||
|
IAddedDate,
|
||||||
|
IRefreshable,
|
||||||
|
ILibraryItem
|
||||||
|
{
|
||||||
|
public static Sort DefaultSort => new Sort<Collection>.By(nameof(Collection.Name));
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of this collection.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The description of this collection.
|
||||||
|
/// </summary>
|
||||||
|
public string? Overview { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Poster { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Thumbnail { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Logo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of movies contained in this collection.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Movie>? Movies { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of shows contained in this collection.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Show>? Shows { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime? NextMetadataRefresh { get; set; }
|
||||||
|
|
||||||
|
public Collection() { }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public Collection(string name)
|
||||||
|
{
|
||||||
|
if (name != null)
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(name);
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
302
src/Kyoo.Abstractions/Models/Resources/Episode.cs
Normal file
302
src/Kyoo.Abstractions/Models/Resources/Episode.cs
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class to represent a single show's episode.
|
||||||
|
/// </summary>
|
||||||
|
public class Episode : IQuery, IResource, IThumbnails, IAddedDate, IRefreshable, INews
|
||||||
|
{
|
||||||
|
// Use absolute numbers by default and fallback to season/episodes if it does not exists.
|
||||||
|
public static Sort DefaultSort =>
|
||||||
|
new Sort<Episode>.Conglomerate(
|
||||||
|
new Sort<Episode>.By(x => x.AbsoluteNumber),
|
||||||
|
new Sort<Episode>.By(x => x.SeasonNumber),
|
||||||
|
new Sort<Episode>.By(x => x.EpisodeNumber)
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[Computed]
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Slug
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (ShowSlug != null || Show?.Slug != null)
|
||||||
|
return GetSlug(ShowSlug ?? Show!.Slug, SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||||
|
return GetSlug(ShowId.ToString(), SeasonNumber, EpisodeNumber, AbsoluteNumber);
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)e(?<episode>\d+)");
|
||||||
|
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
ShowSlug = match.Groups["show"].Value;
|
||||||
|
SeasonNumber = int.Parse(match.Groups["season"].Value);
|
||||||
|
EpisodeNumber = int.Parse(match.Groups["episode"].Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
match = Regex.Match(value, @"(?<show>.+)-(?<absolute>\d+)");
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
ShowSlug = match.Groups["show"].Value;
|
||||||
|
AbsoluteNumber = int.Parse(match.Groups["absolute"].Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
ShowSlug = value;
|
||||||
|
SeasonNumber = null;
|
||||||
|
EpisodeNumber = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The slug of the Show that contain this episode. If this is not set, this episode is ill-formed.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? ShowSlug { private get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the Show containing this episode.
|
||||||
|
/// </summary>
|
||||||
|
public Guid ShowId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The show that contains this episode.
|
||||||
|
/// </summary>
|
||||||
|
[LoadableRelation(nameof(ShowId))]
|
||||||
|
public Show? Show { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the Season containing this episode.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? SeasonId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The season that contains this episode.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This can be null if the season is unknown and the episode is only identified
|
||||||
|
/// by it's <see cref="AbsoluteNumber"/>.
|
||||||
|
/// </remarks>
|
||||||
|
[LoadableRelation(nameof(SeasonId))]
|
||||||
|
public Season? Season { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The season in witch this episode is in.
|
||||||
|
/// </summary>
|
||||||
|
public int? SeasonNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of this episode in it's season.
|
||||||
|
/// </summary>
|
||||||
|
public int? EpisodeNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The absolute number of this episode. It's an episode number that is not reset to 1 after a new season.
|
||||||
|
/// </summary>
|
||||||
|
public int? AbsoluteNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path of the video file for this episode.
|
||||||
|
/// </summary>
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title of this episode.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The overview of this episode.
|
||||||
|
/// </summary>
|
||||||
|
public string? Overview { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long is this episode? (in minutes)
|
||||||
|
/// </summary>
|
||||||
|
public int? Runtime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The release date of this episode. It can be null if unknown.
|
||||||
|
/// </summary>
|
||||||
|
public DateOnly? ReleaseDate { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Poster { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Thumbnail { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Logo { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Dictionary<string, EpisodeId> ExternalId { get; set; } = [];
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime? NextMetadataRefresh { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The previous episode that should be seen before viewing this one.
|
||||||
|
/// </summary>
|
||||||
|
[Projectable(UseMemberBody = nameof(_PreviousEpisode), OnlyOnInclude = true)]
|
||||||
|
[LoadableRelation(
|
||||||
|
// language=PostgreSQL
|
||||||
|
Sql = """
|
||||||
|
select
|
||||||
|
pe.* -- Episode as pe
|
||||||
|
from
|
||||||
|
episodes as "pe"
|
||||||
|
where
|
||||||
|
pe.show_id = "this".show_id
|
||||||
|
and (pe.absolute_number < "this".absolute_number
|
||||||
|
or pe.season_number < "this".season_number
|
||||||
|
or (pe.season_number = "this".season_number
|
||||||
|
and e.episode_number < "this".episode_number))
|
||||||
|
order by
|
||||||
|
pe.absolute_number desc nulls last,
|
||||||
|
pe.season_number desc,
|
||||||
|
pe.episode_number desc
|
||||||
|
limit 1
|
||||||
|
"""
|
||||||
|
)]
|
||||||
|
public Episode? PreviousEpisode { get; set; }
|
||||||
|
|
||||||
|
private Episode? _PreviousEpisode =>
|
||||||
|
Show!
|
||||||
|
.Episodes!.OrderBy(x => x.AbsoluteNumber == null)
|
||||||
|
.ThenByDescending(x => x.AbsoluteNumber)
|
||||||
|
.ThenByDescending(x => x.SeasonNumber)
|
||||||
|
.ThenByDescending(x => x.EpisodeNumber)
|
||||||
|
.FirstOrDefault(x =>
|
||||||
|
x.AbsoluteNumber < AbsoluteNumber
|
||||||
|
|| x.SeasonNumber < SeasonNumber
|
||||||
|
|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber < EpisodeNumber)
|
||||||
|
);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The next episode to watch after this one.
|
||||||
|
/// </summary>
|
||||||
|
[Projectable(UseMemberBody = nameof(_NextEpisode), OnlyOnInclude = true)]
|
||||||
|
[LoadableRelation(
|
||||||
|
// language=PostgreSQL
|
||||||
|
Sql = """
|
||||||
|
select
|
||||||
|
ne.* -- Episode as ne
|
||||||
|
from
|
||||||
|
episodes as "ne"
|
||||||
|
where
|
||||||
|
ne.show_id = "this".show_id
|
||||||
|
and (ne.absolute_number > "this".absolute_number
|
||||||
|
or ne.season_number > "this".season_number
|
||||||
|
or (ne.season_number = "this".season_number
|
||||||
|
and e.episode_number > "this".episode_number))
|
||||||
|
order by
|
||||||
|
ne.absolute_number,
|
||||||
|
ne.season_number,
|
||||||
|
ne.episode_number
|
||||||
|
limit 1
|
||||||
|
"""
|
||||||
|
)]
|
||||||
|
public Episode? NextEpisode { get; set; }
|
||||||
|
|
||||||
|
private Episode? _NextEpisode =>
|
||||||
|
Show!
|
||||||
|
.Episodes!.OrderBy(x => x.AbsoluteNumber)
|
||||||
|
.ThenBy(x => x.SeasonNumber)
|
||||||
|
.ThenBy(x => x.EpisodeNumber)
|
||||||
|
.FirstOrDefault(x =>
|
||||||
|
x.AbsoluteNumber > AbsoluteNumber
|
||||||
|
|| x.SeasonNumber > SeasonNumber
|
||||||
|
|| (x.SeasonNumber == SeasonNumber && x.EpisodeNumber > EpisodeNumber)
|
||||||
|
);
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<EpisodeWatchStatus>? Watched { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata of what an user as started/planned to watch.
|
||||||
|
/// </summary>
|
||||||
|
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||||
|
[LoadableRelation(
|
||||||
|
Sql = "episode_watch_status",
|
||||||
|
On = "episode_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||||
|
)]
|
||||||
|
public EpisodeWatchStatus? WatchStatus { get; set; }
|
||||||
|
|
||||||
|
// There is a global query filter to filter by user so we just need to do single.
|
||||||
|
private EpisodeWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Links to watch this episode.
|
||||||
|
/// </summary>
|
||||||
|
public VideoLinks Links =>
|
||||||
|
new() { Direct = $"/episode/{Slug}/direct", Hls = $"/episode/{Slug}/master.m3u8", };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the slug of an episode.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="showSlug">The slug of the show. It can't be null.</param>
|
||||||
|
/// <param name="seasonNumber">
|
||||||
|
/// The season in which the episode is.
|
||||||
|
/// If this is a movie or if the episode should be referred by it's absolute number, set this to null.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="episodeNumber">
|
||||||
|
/// The number of the episode in it's season.
|
||||||
|
/// If this is a movie or if the episode should be referred by it's absolute number, set this to null.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="absoluteNumber">
|
||||||
|
/// The absolute number of this show.
|
||||||
|
/// If you don't know it or this is a movie, use null
|
||||||
|
/// </param>
|
||||||
|
/// <returns>The slug corresponding to the given arguments</returns>
|
||||||
|
public static string GetSlug(
|
||||||
|
string showSlug,
|
||||||
|
int? seasonNumber,
|
||||||
|
int? episodeNumber,
|
||||||
|
int? absoluteNumber = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return seasonNumber switch
|
||||||
|
{
|
||||||
|
null when absoluteNumber == null => showSlug,
|
||||||
|
null => $"{showSlug}-{absoluteNumber}",
|
||||||
|
_ => $"{showSlug}-s{seasonNumber}e{episodeNumber}"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interface applied to resources.
|
||||||
|
/// </summary>
|
||||||
|
public interface IAddedDate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The date at which this resource was added to kyoo.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interface applied to resources containing external metadata.
|
||||||
|
/// </summary>
|
||||||
|
public interface IMetadata
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The link to metadata providers that this show has. See <see cref="MetadataId"/> for more information.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, MetadataId> ExternalId { get; set; }
|
||||||
|
}
|
||||||
30
src/Kyoo.Abstractions/Models/Resources/Interfaces/IQuery.cs
Normal file
30
src/Kyoo.Abstractions/Models/Resources/Interfaces/IQuery.cs
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
public interface IQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The sorting that will be used when no user defined one is present.
|
||||||
|
/// </summary>
|
||||||
|
public static virtual Sort DefaultSort => throw new NotImplementedException();
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
public interface IRefreshable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The date of the next metadata refresh. Null if auto-refresh is disabled.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? NextMetadataRefresh { get; set; }
|
||||||
|
|
||||||
|
public static DateTime ComputeNextRefreshDate(DateOnly airDate)
|
||||||
|
{
|
||||||
|
int days = DateOnly.FromDateTime(DateTime.UtcNow).DayNumber - airDate.DayNumber;
|
||||||
|
return days switch
|
||||||
|
{
|
||||||
|
<= 4 => DateTime.UtcNow.AddDays(1),
|
||||||
|
<= 21 => DateTime.UtcNow.AddDays(14),
|
||||||
|
_ => DateTime.UtcNow.AddMonths(2)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interface to represent a resource that can be retrieved from the database.
|
||||||
|
/// </summary>
|
||||||
|
public interface IResource : IQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A unique ID for this type of resource. This can't be changed and duplicates are not allowed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// You don't need to specify an ID manually when creating a new resource,
|
||||||
|
/// this field is automatically assigned by the <see cref="IRepository{T}"/>.
|
||||||
|
/// </remarks>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A human-readable identifier that can be used instead of an ID.
|
||||||
|
/// A slug must be unique for a type of resource but it can be changed.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// There is no setter for a slug since it can be computed from other fields.
|
||||||
|
/// For example, a season slug is {ShowSlug}-s{SeasonNumber}.
|
||||||
|
/// </remarks>
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Slug { get; }
|
||||||
|
}
|
||||||
147
src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs
Normal file
147
src/Kyoo.Abstractions/Models/Resources/Interfaces/IThumbnails.cs
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An interface representing items that contains images (like posters, thumbnails, logo, banners...)
|
||||||
|
/// </summary>
|
||||||
|
public interface IThumbnails
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A poster is a 2/3 format image with the cover of the resource.
|
||||||
|
/// </summary>
|
||||||
|
public Image? Poster { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A thumbnail is a 16/9 format image, it could ether be used as a background or as a preview but it usually
|
||||||
|
/// is not an official image.
|
||||||
|
/// </summary>
|
||||||
|
public Image? Thumbnail { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A logo is a small image representing the resource.
|
||||||
|
/// </summary>
|
||||||
|
public Image? Logo { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonConverter(typeof(ImageConvertor))]
|
||||||
|
public class Image
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A unique identifier for the image. Used for proper http caches.
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The original image from another server.
|
||||||
|
/// </summary>
|
||||||
|
public string Source { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A hash to display as placeholder while the image is loading.
|
||||||
|
/// </summary>
|
||||||
|
[MaxLength(32)]
|
||||||
|
public string Blurhash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The url to access the image in low quality.
|
||||||
|
/// </summary>
|
||||||
|
public string Low => $"/thumbnails/{Id}?quality=low";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The url to access the image in medium quality.
|
||||||
|
/// </summary>
|
||||||
|
public string Medium => $"/thumbnails/{Id}?quality=medium";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The url to access the image in high quality.
|
||||||
|
/// </summary>
|
||||||
|
public string High => $"/thumbnails/{Id}?quality=high";
|
||||||
|
|
||||||
|
public Image() { }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public Image(string source, string? blurhash = null)
|
||||||
|
{
|
||||||
|
Source = source;
|
||||||
|
Blurhash = blurhash ?? "000000";
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
public class ImageConvertor : JsonConverter<Image>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override Image? Read(
|
||||||
|
ref Utf8JsonReader reader,
|
||||||
|
Type typeToConvert,
|
||||||
|
JsonSerializerOptions options
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.String && reader.GetString() is string source)
|
||||||
|
return new Image(source);
|
||||||
|
using JsonDocument document = JsonDocument.ParseValue(ref reader);
|
||||||
|
string? src = document.RootElement.GetProperty("Source").GetString();
|
||||||
|
string? blurhash = document.RootElement.GetProperty("Blurhash").GetString();
|
||||||
|
Guid? id = document.RootElement.GetProperty("Id").GetGuid();
|
||||||
|
return new Image(src ?? string.Empty, blurhash) { Id = id ?? Guid.Empty };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override void Write(
|
||||||
|
Utf8JsonWriter writer,
|
||||||
|
Image value,
|
||||||
|
JsonSerializerOptions options
|
||||||
|
)
|
||||||
|
{
|
||||||
|
writer.WriteStartObject();
|
||||||
|
writer.WriteString("source", value.Source);
|
||||||
|
writer.WriteString("blurhash", value.Blurhash);
|
||||||
|
writer.WriteString("low", value.Low);
|
||||||
|
writer.WriteString("medium", value.Medium);
|
||||||
|
writer.WriteString("high", value.High);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The quality of an image
|
||||||
|
/// </summary>
|
||||||
|
public enum ImageQuality
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Small
|
||||||
|
/// </summary>
|
||||||
|
Low,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Medium
|
||||||
|
/// </summary>
|
||||||
|
Medium,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Large
|
||||||
|
/// </summary>
|
||||||
|
High,
|
||||||
|
}
|
||||||
65
src/Kyoo.Abstractions/Models/Resources/JwtToken.cs
Normal file
65
src/Kyoo.Abstractions/Models/Resources/JwtToken.cs
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A container representing the response of a login or token refresh.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Initializes a new instance of the <see cref="JwtToken"/> class.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="accessToken">The access token used to authorize requests.</param>
|
||||||
|
/// <param name="refreshToken">The refresh token to retrieve a new access token.</param>
|
||||||
|
/// <param name="expireIn">When the access token will expire.</param>
|
||||||
|
public class JwtToken(string accessToken, string refreshToken, TimeSpan expireIn)
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type of this token (always a Bearer).
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("token_type")]
|
||||||
|
public string TokenType => "Bearer";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The access token used to authorize requests.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("access_token")]
|
||||||
|
public string AccessToken { get; set; } = accessToken;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The refresh token used to retrieve a new access/refresh token when the access token has expired.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("refresh_token")]
|
||||||
|
public string RefreshToken { get; set; } = refreshToken;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When the access token will expire. After this time, the refresh token should be used to retrieve.
|
||||||
|
/// a new token.cs
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("expire_in")]
|
||||||
|
public TimeSpan ExpireIn => ExpireAt.Subtract(DateTime.UtcNow);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The exact date at which the access token will expire.
|
||||||
|
/// </summary>
|
||||||
|
[JsonPropertyName("expire_at")]
|
||||||
|
public DateTime ExpireAt { get; set; } = DateTime.UtcNow + expireIn;
|
||||||
|
}
|
||||||
192
src/Kyoo.Abstractions/Models/Resources/Movie.cs
Normal file
192
src/Kyoo.Abstractions/Models/Resources/Movie.cs
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A series or a movie.
|
||||||
|
/// </summary>
|
||||||
|
public class Movie
|
||||||
|
: IQuery,
|
||||||
|
IResource,
|
||||||
|
IMetadata,
|
||||||
|
IThumbnails,
|
||||||
|
IAddedDate,
|
||||||
|
IRefreshable,
|
||||||
|
ILibraryItem,
|
||||||
|
INews,
|
||||||
|
IWatchlist
|
||||||
|
{
|
||||||
|
public static Sort DefaultSort => new Sort<Movie>.By(x => x.Name);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title of this show.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A catchphrase for this movie.
|
||||||
|
/// </summary>
|
||||||
|
public string? Tagline { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of alternative titles of this show.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Aliases { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The path of the movie video file.
|
||||||
|
/// </summary>
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The summary of this show.
|
||||||
|
/// </summary>
|
||||||
|
public string? Overview { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A list of tags that match this movie.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Tags { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of genres (themes) this show has.
|
||||||
|
/// </summary>
|
||||||
|
public List<Genre> Genres { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this show airing, not aired yet or finished?
|
||||||
|
/// </summary>
|
||||||
|
public Status Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How well this item is rated? (from 0 to 100).
|
||||||
|
/// </summary>
|
||||||
|
public int Rating { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How long is this movie? (in minutes)
|
||||||
|
/// </summary>
|
||||||
|
public int? Runtime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date this movie aired.
|
||||||
|
/// </summary>
|
||||||
|
public DateOnly? AirDate { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Poster { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Thumbnail { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Logo { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[Column("air_date")]
|
||||||
|
public DateOnly? StartAir => AirDate;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[Column("air_date")]
|
||||||
|
public DateOnly? EndAir => AirDate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A video of a few minutes that tease the content.
|
||||||
|
/// </summary>
|
||||||
|
public string? Trailer { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime? NextMetadataRefresh { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the Studio that made this show.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public Guid? StudioId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Studio that made this show.
|
||||||
|
/// </summary>
|
||||||
|
[LoadableRelation(nameof(StudioId))]
|
||||||
|
public Studio? Studio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of collections that contains this show.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Collection>? Collections { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Links to watch this movie.
|
||||||
|
/// </summary>
|
||||||
|
public VideoLinks Links =>
|
||||||
|
new() { Direct = $"/movie/{Slug}/direct", Hls = $"/movie/{Slug}/master.m3u8", };
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<MovieWatchStatus>? Watched { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata of what an user as started/planned to watch.
|
||||||
|
/// </summary>
|
||||||
|
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||||
|
[LoadableRelation(
|
||||||
|
Sql = "movie_watch_status",
|
||||||
|
On = "movie_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||||
|
)]
|
||||||
|
public MovieWatchStatus? WatchStatus { get; set; }
|
||||||
|
|
||||||
|
// There is a global query filter to filter by user so we just need to do single.
|
||||||
|
private MovieWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||||
|
|
||||||
|
public Movie() { }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public Movie(string name)
|
||||||
|
{
|
||||||
|
if (name != null)
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(name);
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
151
src/Kyoo.Abstractions/Models/Resources/Season.cs
Normal file
151
src/Kyoo.Abstractions/Models/Resources/Season.cs
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A season of a <see cref="Show"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class Season : IQuery, IResource, IMetadata, IThumbnails, IAddedDate, IRefreshable
|
||||||
|
{
|
||||||
|
public static Sort DefaultSort => new Sort<Season>.By(x => x.SeasonNumber);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[Computed]
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Slug
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (ShowSlug == null && Show == null)
|
||||||
|
return $"{ShowId}-s{SeasonNumber}";
|
||||||
|
return $"{ShowSlug ?? Show?.Slug}-s{SeasonNumber}";
|
||||||
|
}
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
Match match = Regex.Match(value, @"(?<show>.+)-s(?<season>\d+)");
|
||||||
|
|
||||||
|
if (!match.Success)
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Invalid season slug. Format: {showSlug}-s{seasonNumber}"
|
||||||
|
);
|
||||||
|
ShowSlug = match.Groups["show"].Value;
|
||||||
|
SeasonNumber = int.Parse(match.Groups["season"].Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The slug of the Show that contain this episode. If this is not set, this season is ill-formed.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? ShowSlug { private get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the Show containing this season.
|
||||||
|
/// </summary>
|
||||||
|
public Guid ShowId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The show that contains this season.
|
||||||
|
/// </summary>
|
||||||
|
[LoadableRelation(nameof(ShowId))]
|
||||||
|
public Show? Show { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of this season. This can be set to 0 to indicate specials.
|
||||||
|
/// </summary>
|
||||||
|
public int SeasonNumber { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title of this season.
|
||||||
|
/// </summary>
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A quick overview of this season.
|
||||||
|
/// </summary>
|
||||||
|
public string? Overview { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The starting air date of this season.
|
||||||
|
/// </summary>
|
||||||
|
public DateOnly? StartDate { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ending date of this season.
|
||||||
|
/// </summary>
|
||||||
|
public DateOnly? EndDate { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Poster { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Thumbnail { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Logo { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime? NextMetadataRefresh { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of episodes that this season contains.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Episode>? Episodes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of episodes in this season.
|
||||||
|
/// </summary>
|
||||||
|
[Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
|
||||||
|
[NotMapped]
|
||||||
|
[LoadableRelation(
|
||||||
|
// language=PostgreSQL
|
||||||
|
Projected = """
|
||||||
|
(
|
||||||
|
select
|
||||||
|
count(*)::int
|
||||||
|
from
|
||||||
|
episodes as e
|
||||||
|
where
|
||||||
|
e.season_id = id) as episodes_count
|
||||||
|
"""
|
||||||
|
)]
|
||||||
|
public int EpisodesCount { get; set; }
|
||||||
|
|
||||||
|
private int _EpisodesCount => Episodes!.Count;
|
||||||
|
}
|
||||||
283
src/Kyoo.Abstractions/Models/Resources/Show.cs
Normal file
283
src/Kyoo.Abstractions/Models/Resources/Show.cs
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using EntityFrameworkCore.Projectables;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A series or a movie.
|
||||||
|
/// </summary>
|
||||||
|
public class Show
|
||||||
|
: IQuery,
|
||||||
|
IResource,
|
||||||
|
IMetadata,
|
||||||
|
IOnMerge,
|
||||||
|
IThumbnails,
|
||||||
|
IAddedDate,
|
||||||
|
IRefreshable,
|
||||||
|
ILibraryItem,
|
||||||
|
IWatchlist
|
||||||
|
{
|
||||||
|
public static Sort DefaultSort => new Sort<Show>.By(x => x.Name);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The title of this show.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A catchphrase for this show.
|
||||||
|
/// </summary>
|
||||||
|
public string? Tagline { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of alternative titles of this show.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Aliases { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The summary of this show.
|
||||||
|
/// </summary>
|
||||||
|
public string? Overview { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A list of tags that match this movie.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Tags { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of genres (themes) this show has.
|
||||||
|
/// </summary>
|
||||||
|
public List<Genre> Genres { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is this show airing, not aired yet or finished?
|
||||||
|
/// </summary>
|
||||||
|
public Status Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// How well this item is rated? (from 0 to 100).
|
||||||
|
/// </summary>
|
||||||
|
public int Rating { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date this show started airing. It can be null if this is unknown.
|
||||||
|
/// </summary>
|
||||||
|
public DateOnly? StartAir { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date this show finished airing.
|
||||||
|
/// It can also be null if this is unknown.
|
||||||
|
/// </summary>
|
||||||
|
public DateOnly? EndAir { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Poster { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Thumbnail { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Image? Logo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A video of a few minutes that tease the content.
|
||||||
|
/// </summary>
|
||||||
|
public string? Trailer { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
[Column("start_air")]
|
||||||
|
public DateOnly? AirDate => StartAir;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime? NextMetadataRefresh { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the Studio that made this show.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? StudioId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The Studio that made this show.
|
||||||
|
/// </summary>
|
||||||
|
[LoadableRelation(nameof(StudioId))]
|
||||||
|
public Studio? Studio { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The different seasons in this show. If this is a movie, this list is always null or empty.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Season>? Seasons { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of episodes in this show.
|
||||||
|
/// If this is a movie, there will be a unique episode (with the seasonNumber and episodeNumber set to null).
|
||||||
|
/// Having an episode is necessary to store metadata and tracks.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Episode>? Episodes { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of collections that contains this show.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Collection>? Collections { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The first episode of this show.
|
||||||
|
/// </summary>
|
||||||
|
[Projectable(UseMemberBody = nameof(_FirstEpisode), OnlyOnInclude = true)]
|
||||||
|
[LoadableRelation(
|
||||||
|
// language=PostgreSQL
|
||||||
|
Sql = """
|
||||||
|
select
|
||||||
|
fe.* -- Episode as fe
|
||||||
|
from (
|
||||||
|
select
|
||||||
|
e.*,
|
||||||
|
row_number() over (partition by e.show_id order by e.absolute_number, e.season_number, e.episode_number) as number
|
||||||
|
from
|
||||||
|
episodes as e) as "fe"
|
||||||
|
where
|
||||||
|
fe.number <= 1
|
||||||
|
""",
|
||||||
|
On = "show_id = \"this\".id"
|
||||||
|
)]
|
||||||
|
public Episode? FirstEpisode { get; set; }
|
||||||
|
|
||||||
|
private Episode? _FirstEpisode =>
|
||||||
|
Episodes!
|
||||||
|
.OrderBy(x => x.AbsoluteNumber)
|
||||||
|
.ThenBy(x => x.SeasonNumber)
|
||||||
|
.ThenBy(x => x.EpisodeNumber)
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of episodes in this show.
|
||||||
|
/// </summary>
|
||||||
|
[Projectable(UseMemberBody = nameof(_EpisodesCount), OnlyOnInclude = true)]
|
||||||
|
[NotMapped]
|
||||||
|
[LoadableRelation(
|
||||||
|
// language=PostgreSQL
|
||||||
|
Projected = """
|
||||||
|
(
|
||||||
|
select
|
||||||
|
count(*)::int
|
||||||
|
from
|
||||||
|
episodes as e
|
||||||
|
where
|
||||||
|
e.show_id = "this".id) as episodes_count
|
||||||
|
"""
|
||||||
|
)]
|
||||||
|
public int EpisodesCount { get; set; }
|
||||||
|
|
||||||
|
private int _EpisodesCount => Episodes!.Count;
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<ShowWatchStatus>? Watched { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata of what an user as started/planned to watch.
|
||||||
|
/// </summary>
|
||||||
|
[Projectable(UseMemberBody = nameof(_WatchStatus), OnlyOnInclude = true)]
|
||||||
|
[LoadableRelation(
|
||||||
|
Sql = "show_watch_status",
|
||||||
|
On = "show_id = \"this\".id and \"relation\".user_id = [current_user]"
|
||||||
|
)]
|
||||||
|
public ShowWatchStatus? WatchStatus { get; set; }
|
||||||
|
|
||||||
|
// There is a global query filter to filter by user so we just need to do single.
|
||||||
|
private ShowWatchStatus? _WatchStatus => Watched!.FirstOrDefault();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void OnMerge(object merged)
|
||||||
|
{
|
||||||
|
if (Seasons != null)
|
||||||
|
{
|
||||||
|
foreach (Season season in Seasons)
|
||||||
|
season.Show = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Episodes != null)
|
||||||
|
{
|
||||||
|
foreach (Episode episode in Episodes)
|
||||||
|
episode.Show = this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Show() { }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public Show(string name)
|
||||||
|
{
|
||||||
|
if (name != null)
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(name);
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The enum containing show's status.
|
||||||
|
/// </summary>
|
||||||
|
public enum Status
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The status of the show is not known.
|
||||||
|
/// </summary>
|
||||||
|
Unknown,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The show has finished airing.
|
||||||
|
/// </summary>
|
||||||
|
Finished,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The show is still actively airing.
|
||||||
|
/// </summary>
|
||||||
|
Airing,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// This show has not aired yet but has been announced.
|
||||||
|
/// </summary>
|
||||||
|
Planned
|
||||||
|
}
|
||||||
80
src/Kyoo.Abstractions/Models/Resources/Studio.cs
Normal file
80
src/Kyoo.Abstractions/Models/Resources/Studio.cs
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A studio that make shows.
|
||||||
|
/// </summary>
|
||||||
|
public class Studio : IQuery, IResource, IMetadata
|
||||||
|
{
|
||||||
|
public static Sort DefaultSort => new Sort<Studio>.By(x => x.Name);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of this studio.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of shows that are made by this studio.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Show>? Shows { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of movies that are made by this studio.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public ICollection<Movie>? Movies { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Dictionary<string, MetadataId> ExternalId { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new, empty, <see cref="Studio"/>.
|
||||||
|
/// </summary>
|
||||||
|
public Studio() { }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Studio"/> with a specific name, the slug is calculated automatically.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The name of the studio.</param>
|
||||||
|
[JsonConstructor]
|
||||||
|
public Studio(string name)
|
||||||
|
{
|
||||||
|
if (name != null)
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(name);
|
||||||
|
Name = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/Kyoo.Abstractions/Models/Resources/User.cs
Normal file
116
src/Kyoo.Abstractions/Models/Resources/User.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A single user of the app.
|
||||||
|
/// </summary>
|
||||||
|
public class User : IQuery, IResource, IAddedDate
|
||||||
|
{
|
||||||
|
public static Sort DefaultSort => new Sort<User>.By(x => x.Username);
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
[MaxLength(256)]
|
||||||
|
public string Slug { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A username displayed to the user.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user email address.
|
||||||
|
/// </summary>
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user password (hashed, it can't be read like that). The hashing format is implementation defined.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Does the user can sign-in with a password or only via oidc?
|
||||||
|
/// </summary>
|
||||||
|
public bool HasPassword => Password != null;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of permissions of the user. The format of this is implementation dependent.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Permissions { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User settings
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> Settings { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// User accounts on other services.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, ExternalToken> ExternalId { get; set; } = new();
|
||||||
|
|
||||||
|
public User() { }
|
||||||
|
|
||||||
|
[JsonConstructor]
|
||||||
|
public User(string username)
|
||||||
|
{
|
||||||
|
if (username != null)
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(username);
|
||||||
|
Username = username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ExternalToken
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The id of this user on the external service.
|
||||||
|
/// </summary>
|
||||||
|
public string Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The username on the external service.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The link to the user profile on this website. Null if it does not exist.
|
||||||
|
/// </summary>
|
||||||
|
public string? ProfileUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A jwt token used to interact with the service.
|
||||||
|
/// Do not forget to refresh it when using it if necessary.
|
||||||
|
/// </summary>
|
||||||
|
public JwtToken Token { get; set; }
|
||||||
|
}
|
||||||
279
src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs
Normal file
279
src/Kyoo.Abstractions/Models/Resources/WatchStatus.cs
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has the user started watching, is it planned?
|
||||||
|
/// </summary>
|
||||||
|
public enum WatchStatus
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user has already watched this.
|
||||||
|
/// </summary>
|
||||||
|
Completed,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user started watching this but has not finished.
|
||||||
|
/// </summary>
|
||||||
|
Watching,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user does not plan to continue watching.
|
||||||
|
/// </summary>
|
||||||
|
Droped,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user has not started watching this but plans to.
|
||||||
|
/// </summary>
|
||||||
|
Planned,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The watch status was deleted and can not be retrived again.
|
||||||
|
/// </summary>
|
||||||
|
Deleted,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metadata of what an user as started/planned to watch.
|
||||||
|
/// </summary>
|
||||||
|
[SqlFirstColumn(nameof(UserId))]
|
||||||
|
public class MovieWatchStatus : IAddedDate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the user that started watching this episode.
|
||||||
|
/// </summary>
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user that started watching this episode.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public User User { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the movie started.
|
||||||
|
/// </summary>
|
||||||
|
public Guid MovieId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="Movie"/> started.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public Movie Movie { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date at which this item was played.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PlayedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has the user started watching, is it planned?
|
||||||
|
/// </summary>
|
||||||
|
public WatchStatus Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the player has stopped watching the movie (in seconds).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Null if the status is not Watching.
|
||||||
|
/// </remarks>
|
||||||
|
public int? WatchedTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the player has stopped watching the movie (in percentage between 0 and 100).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Null if the status is not Watching.
|
||||||
|
/// </remarks>
|
||||||
|
public int? WatchedPercent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[SqlFirstColumn(nameof(UserId))]
|
||||||
|
public class EpisodeWatchStatus : IAddedDate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the user that started watching this episode.
|
||||||
|
/// </summary>
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user that started watching this episode.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public User User { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the episode started.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? EpisodeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="Episode"/> started.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public Episode Episode { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date at which this item was played.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PlayedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has the user started watching, is it planned?
|
||||||
|
/// </summary>
|
||||||
|
public WatchStatus Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the player has stopped watching the episode (in seconds).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Null if the status is not Watching.
|
||||||
|
/// </remarks>
|
||||||
|
public int? WatchedTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Null if the status is not Watching or if the next episode is not started.
|
||||||
|
/// </remarks>
|
||||||
|
public int? WatchedPercent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[SqlFirstColumn(nameof(UserId))]
|
||||||
|
public class ShowWatchStatus : IAddedDate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the user that started watching this episode.
|
||||||
|
/// </summary>
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user that started watching this episode.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public User User { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the show started.
|
||||||
|
/// </summary>
|
||||||
|
public Guid ShowId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The <see cref="Show"/> started.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public Show Show { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date at which this item was played.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PlayedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Has the user started watching, is it planned?
|
||||||
|
/// </summary>
|
||||||
|
public WatchStatus Status { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The number of episodes the user has not seen.
|
||||||
|
/// </summary>
|
||||||
|
public int UnseenEpisodesCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the episode started.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? NextEpisodeId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The next <see cref="Episode"/> to watch.
|
||||||
|
/// </summary>
|
||||||
|
public Episode? NextEpisode { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the player has stopped watching the episode (in seconds).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Null if the status is not Watching or if the next episode is not started.
|
||||||
|
/// </remarks>
|
||||||
|
public int? WatchedTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Null if the status is not Watching or if the next episode is not started.
|
||||||
|
/// </remarks>
|
||||||
|
public int? WatchedPercent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class WatchStatus<T> : IAddedDate
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Has the user started watching, is it planned?
|
||||||
|
/// </summary>
|
||||||
|
public required WatchStatus Status { get; set; }
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public DateTime AddedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The date at which this item was played.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? PlayedDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the player has stopped watching the episode (in seconds).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Null if the status is not Watching or if the next episode is not started.
|
||||||
|
/// </remarks>
|
||||||
|
public int? WatchedTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where the player has stopped watching the episode (in percentage between 0 and 100).
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Null if the status is not Watching or if the next episode is not started.
|
||||||
|
/// </remarks>
|
||||||
|
public int? WatchedPercent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user that started watching this episode.
|
||||||
|
/// </summary>
|
||||||
|
public required User User { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The episode/show/movie whose status changed
|
||||||
|
/// </summary>
|
||||||
|
public required T Resource { get; set; }
|
||||||
|
}
|
||||||
53
src/Kyoo.Abstractions/Models/SearchPage.cs
Normal file
53
src/Kyoo.Abstractions/Models/SearchPage.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Results of a search request.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The search item's type.</typeparam>
|
||||||
|
public class SearchPage<T> : Page<T>
|
||||||
|
where T : IResource
|
||||||
|
{
|
||||||
|
public SearchPage(
|
||||||
|
SearchResult result,
|
||||||
|
string @this,
|
||||||
|
string? previous,
|
||||||
|
string? next,
|
||||||
|
string first
|
||||||
|
)
|
||||||
|
: base(result.Items, @this, previous, next, first)
|
||||||
|
{
|
||||||
|
Query = result.Query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The query of the search request.
|
||||||
|
/// </summary>
|
||||||
|
public string? Query { get; init; }
|
||||||
|
|
||||||
|
public class SearchResult
|
||||||
|
{
|
||||||
|
public string? Query { get; set; }
|
||||||
|
|
||||||
|
public ICollection<T> Items { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/Kyoo.Abstractions/Models/Utils/Claims.cs
Normal file
55
src/Kyoo.Abstractions/Models/Utils/Claims.cs
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// List of well known claims of kyoo
|
||||||
|
/// </summary>
|
||||||
|
public static class Claims
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The id of the user
|
||||||
|
/// </summary>
|
||||||
|
public static string Id => "id";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the user
|
||||||
|
/// </summary>
|
||||||
|
public static string Name => "name";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The email of the user.
|
||||||
|
/// </summary>
|
||||||
|
public static string Email => "email";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of permissions that the user has.
|
||||||
|
/// </summary>
|
||||||
|
public static string Permissions => "permissions";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type of the token (either "access" or "refresh").
|
||||||
|
/// </summary>
|
||||||
|
public static string Type => "type";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A guid used to identify a specific refresh token. This is only useful for the server to revokate tokens.
|
||||||
|
/// </summary>
|
||||||
|
public static string Guid => "guid";
|
||||||
|
}
|
||||||
60
src/Kyoo.Abstractions/Models/Utils/Constants.cs
Normal file
60
src/Kyoo.Abstractions/Models/Utils/Constants.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class containing constant numbers.
|
||||||
|
/// </summary>
|
||||||
|
public static class Constants
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A property to use on a Microsoft.AspNet.MVC.Route.Order property to mark it as an alternative route
|
||||||
|
/// that won't be included on the swagger.
|
||||||
|
/// </summary>
|
||||||
|
public const int AlternativeRoute = 1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by users.
|
||||||
|
/// </summary>
|
||||||
|
public const string UsersGroup = "0:Users";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for main resources of kyoo.
|
||||||
|
/// </summary>
|
||||||
|
public const string ResourcesGroup = "1:Resources";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A group name for <see cref="ApiDefinitionAttribute"/>.
|
||||||
|
/// It should be used for sub resources of kyoo that help define the main resources.
|
||||||
|
/// </summary>
|
||||||
|
public const string MetadataGroup = "2:Metadata";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints useful for playback.
|
||||||
|
/// </summary>
|
||||||
|
public const string WatchGroup = "3:Watch";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A group name for <see cref="ApiDefinitionAttribute"/>. It should be used for endpoints used by admins.
|
||||||
|
/// </summary>
|
||||||
|
public const string AdminGroup = "4:Admin";
|
||||||
|
public const string OtherGroup = "5:Other";
|
||||||
|
}
|
||||||
369
src/Kyoo.Abstractions/Models/Utils/Filter.cs
Normal file
369
src/Kyoo.Abstractions/Models/Utils/Filter.cs
Normal file
@@ -0,0 +1,369 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
using Sprache;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
public static class ParseHelper
|
||||||
|
{
|
||||||
|
public static Parser<T> ErrorMessage<T>(this Parser<T> @this, string message) =>
|
||||||
|
input =>
|
||||||
|
{
|
||||||
|
IResult<T> result = @this(input);
|
||||||
|
|
||||||
|
return result.WasSuccessful
|
||||||
|
? result
|
||||||
|
: Result.Failure<T>(result.Remainder, message, result.Expectations);
|
||||||
|
};
|
||||||
|
|
||||||
|
public static Parser<T> Error<T>(string message) =>
|
||||||
|
input =>
|
||||||
|
{
|
||||||
|
return Result.Failure<T>(input, message, Array.Empty<string>());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract record Filter
|
||||||
|
{
|
||||||
|
public static Filter<T>? And<T>(params Filter<T>?[] filters)
|
||||||
|
{
|
||||||
|
return filters
|
||||||
|
.Where(x => x != null)
|
||||||
|
.Aggregate(
|
||||||
|
(Filter<T>?)null,
|
||||||
|
(acc, filter) =>
|
||||||
|
{
|
||||||
|
if (acc == null)
|
||||||
|
return filter;
|
||||||
|
return new Filter<T>.And(acc, filter!);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Filter<T>? Or<T>(params Filter<T>?[] filters)
|
||||||
|
{
|
||||||
|
return filters
|
||||||
|
.Where(x => x != null)
|
||||||
|
.Aggregate(
|
||||||
|
(Filter<T>?)null,
|
||||||
|
(acc, filter) =>
|
||||||
|
{
|
||||||
|
if (acc == null)
|
||||||
|
return filter;
|
||||||
|
return new Filter<T>.Or(acc, filter!);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract record Filter<T> : Filter
|
||||||
|
{
|
||||||
|
public record And(Filter<T> First, Filter<T> Second) : Filter<T>;
|
||||||
|
|
||||||
|
public record Or(Filter<T> First, Filter<T> Second) : Filter<T>;
|
||||||
|
|
||||||
|
public record Not(Filter<T> Filter) : Filter<T>;
|
||||||
|
|
||||||
|
public record Eq(string Property, object? Value) : Filter<T>;
|
||||||
|
|
||||||
|
public record Ne(string Property, object? Value) : Filter<T>;
|
||||||
|
|
||||||
|
public record Gt(string Property, object Value) : Filter<T>;
|
||||||
|
|
||||||
|
public record Ge(string Property, object Value) : Filter<T>;
|
||||||
|
|
||||||
|
public record Lt(string Property, object Value) : Filter<T>;
|
||||||
|
|
||||||
|
public record Le(string Property, object Value) : Filter<T>;
|
||||||
|
|
||||||
|
public record Has(string Property, object Value) : Filter<T>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal filter used for keyset paginations to resume random sorts.
|
||||||
|
/// The pseudo sql is md5(seed || table.id) = md5(seed || 'hardCodedId')
|
||||||
|
/// </summary>
|
||||||
|
public record CmpRandom(string cmp, string Seed, Guid ReferenceId) : Filter<T>;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal filter used only in EF with hard coded lamdas (used for relations).
|
||||||
|
/// </summary>
|
||||||
|
public record Lambda(Expression<Func<T, bool>> Inner) : Filter<T>;
|
||||||
|
|
||||||
|
public static class FilterParsers
|
||||||
|
{
|
||||||
|
public static readonly Parser<Filter<T>> Filter = Parse
|
||||||
|
.Ref(() => Bracket)
|
||||||
|
.Or(Parse.Ref(() => Not))
|
||||||
|
.Or(Parse.Ref(() => Eq))
|
||||||
|
.Or(Parse.Ref(() => Ne))
|
||||||
|
.Or(Parse.Ref(() => Gt))
|
||||||
|
.Or(Parse.Ref(() => Ge))
|
||||||
|
.Or(Parse.Ref(() => Lt))
|
||||||
|
.Or(Parse.Ref(() => Le))
|
||||||
|
.Or(Parse.Ref(() => Has));
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> CompleteFilter = Parse
|
||||||
|
.Ref(() => Or)
|
||||||
|
.Or(Parse.Ref(() => And))
|
||||||
|
.Or(Filter);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Bracket =
|
||||||
|
from open in Parse.Char('(').Token()
|
||||||
|
from filter in CompleteFilter
|
||||||
|
from close in Parse.Char(')').Token()
|
||||||
|
select filter;
|
||||||
|
|
||||||
|
public static readonly Parser<IEnumerable<char>> AndOperator = Parse
|
||||||
|
.IgnoreCase("and")
|
||||||
|
.Or(Parse.String("&&"))
|
||||||
|
.Token();
|
||||||
|
|
||||||
|
public static readonly Parser<IEnumerable<char>> OrOperator = Parse
|
||||||
|
.IgnoreCase("or")
|
||||||
|
.Or(Parse.String("||"))
|
||||||
|
.Token();
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> And = Parse.ChainOperator(
|
||||||
|
AndOperator,
|
||||||
|
Filter,
|
||||||
|
(_, a, b) => new And(a, b)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Or = Parse.ChainOperator(
|
||||||
|
OrOperator,
|
||||||
|
And.Or(Filter),
|
||||||
|
(_, a, b) => new Or(a, b)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Not =
|
||||||
|
from not in Parse.IgnoreCase("not").Or(Parse.String("!")).Token()
|
||||||
|
from filter in CompleteFilter
|
||||||
|
select new Not(filter);
|
||||||
|
|
||||||
|
private static Parser<object> _GetValueParser(Type type)
|
||||||
|
{
|
||||||
|
Type? nullable = Nullable.GetUnderlyingType(type);
|
||||||
|
if (nullable != null)
|
||||||
|
{
|
||||||
|
return from value in _GetValueParser(nullable) select value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == typeof(int))
|
||||||
|
return Parse.Number.Select(x => int.Parse(x) as object);
|
||||||
|
|
||||||
|
if (type == typeof(float))
|
||||||
|
{
|
||||||
|
return from a in Parse.Number
|
||||||
|
from dot in Parse.Char('.')
|
||||||
|
from b in Parse.Number
|
||||||
|
select float.Parse($"{a}.{b}") as object;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == typeof(Guid))
|
||||||
|
{
|
||||||
|
return from guid in Parse.Regex(
|
||||||
|
@"[({]?[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12}[})]?",
|
||||||
|
"Guid"
|
||||||
|
)
|
||||||
|
select Guid.Parse(guid) as object;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == typeof(string))
|
||||||
|
{
|
||||||
|
return (
|
||||||
|
from lq in Parse.Char('"').Or(Parse.Char('\''))
|
||||||
|
from str in Parse.AnyChar.Where(x => x != lq).Many().Text()
|
||||||
|
from rq in Parse.Char(lq)
|
||||||
|
select str
|
||||||
|
).Or(Parse.LetterOrDigit.Many().Text());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.IsEnum)
|
||||||
|
{
|
||||||
|
return Parse
|
||||||
|
.LetterOrDigit.Many()
|
||||||
|
.Text()
|
||||||
|
.Then(x =>
|
||||||
|
{
|
||||||
|
if (Enum.TryParse(type, x, true, out object? value))
|
||||||
|
return Parse.Return(value);
|
||||||
|
return ParseHelper.Error<object>($"Invalid enum value. Unexpected {x}");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == typeof(DateTime) || type == typeof(DateOnly))
|
||||||
|
{
|
||||||
|
return from year in Parse.Digit.Repeat(4).Text().Select(int.Parse)
|
||||||
|
from yd in Parse.Char('-')
|
||||||
|
from month in Parse.Digit.Repeat(2).Text().Select(int.Parse)
|
||||||
|
from md in Parse.Char('-')
|
||||||
|
from day in Parse.Digit.Repeat(2).Text().Select(int.Parse)
|
||||||
|
select type == typeof(DateTime)
|
||||||
|
? new DateTime(year, month, day) as object
|
||||||
|
: new DateOnly(year, month, day) as object;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof(IEnumerable).IsAssignableFrom(type))
|
||||||
|
return ParseHelper.Error<object>(
|
||||||
|
"Can't filter a list with a default comparator, use the 'has' filter."
|
||||||
|
);
|
||||||
|
return ParseHelper.Error<object>("Unfilterable field found");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Parser<Filter<T>> _GetOperationParser(
|
||||||
|
Parser<object> op,
|
||||||
|
Func<string, object, Filter<T>> apply,
|
||||||
|
Func<Type, Parser<object?>>? customTypeParser = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Parser<string> property = Parse.LetterOrDigit.AtLeastOnce().Text();
|
||||||
|
|
||||||
|
return property.Then(prop =>
|
||||||
|
{
|
||||||
|
Type[] types =
|
||||||
|
typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
|
||||||
|
|
||||||
|
if (string.Equals(prop, "kind", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
return from eq in op
|
||||||
|
from val in types
|
||||||
|
.Select(x => Parse.IgnoreCase(x.Name).Text())
|
||||||
|
.Aggregate(
|
||||||
|
null as Parser<string>,
|
||||||
|
(acc, x) => acc == null ? x : Parse.Or(acc, x)
|
||||||
|
)
|
||||||
|
select apply("kind", val);
|
||||||
|
}
|
||||||
|
|
||||||
|
PropertyInfo? propInfo = types
|
||||||
|
.Select(x =>
|
||||||
|
x.GetProperty(
|
||||||
|
prop,
|
||||||
|
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.FirstOrDefault();
|
||||||
|
if (propInfo == null)
|
||||||
|
return ParseHelper.Error<Filter<T>>($"The given filter '{prop}' is invalid.");
|
||||||
|
|
||||||
|
Parser<object?> value =
|
||||||
|
customTypeParser != null
|
||||||
|
? customTypeParser(propInfo.PropertyType)
|
||||||
|
: _GetValueParser(propInfo.PropertyType);
|
||||||
|
|
||||||
|
return from eq in op
|
||||||
|
from val in value
|
||||||
|
select apply(propInfo.Name, val);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Eq = _GetOperationParser(
|
||||||
|
Parse.IgnoreCase("eq").Or(Parse.String("=")).Token(),
|
||||||
|
(property, value) => new Eq(property, value),
|
||||||
|
(Type type) =>
|
||||||
|
{
|
||||||
|
Type? inner = Nullable.GetUnderlyingType(type);
|
||||||
|
if (inner == null)
|
||||||
|
return _GetValueParser(type);
|
||||||
|
return Parse
|
||||||
|
.String("null")
|
||||||
|
.Token()
|
||||||
|
.Return((object?)null)
|
||||||
|
.Or(_GetValueParser(inner));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Ne = _GetOperationParser(
|
||||||
|
Parse.IgnoreCase("ne").Or(Parse.String("!=")).Token(),
|
||||||
|
(property, value) => new Ne(property, value),
|
||||||
|
(Type type) =>
|
||||||
|
{
|
||||||
|
Type? inner = Nullable.GetUnderlyingType(type);
|
||||||
|
if (inner == null)
|
||||||
|
return _GetValueParser(type);
|
||||||
|
return Parse
|
||||||
|
.String("null")
|
||||||
|
.Token()
|
||||||
|
.Return((object?)null)
|
||||||
|
.Or(_GetValueParser(inner));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Gt = _GetOperationParser(
|
||||||
|
Parse.IgnoreCase("gt").Or(Parse.String(">")).Token(),
|
||||||
|
(property, value) => new Gt(property, value)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Ge = _GetOperationParser(
|
||||||
|
Parse.IgnoreCase("ge").Or(Parse.IgnoreCase("gte")).Or(Parse.String(">=")).Token(),
|
||||||
|
(property, value) => new Ge(property, value)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Lt = _GetOperationParser(
|
||||||
|
Parse.IgnoreCase("lt").Or(Parse.String("<")).Token(),
|
||||||
|
(property, value) => new Lt(property, value)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Le = _GetOperationParser(
|
||||||
|
Parse.IgnoreCase("le").Or(Parse.IgnoreCase("lte")).Or(Parse.String("<=")).Token(),
|
||||||
|
(property, value) => new Le(property, value)
|
||||||
|
);
|
||||||
|
|
||||||
|
public static readonly Parser<Filter<T>> Has = _GetOperationParser(
|
||||||
|
Parse.IgnoreCase("has").Token(),
|
||||||
|
(property, value) => new Has(property, value),
|
||||||
|
(Type type) =>
|
||||||
|
{
|
||||||
|
if (typeof(IEnumerable).IsAssignableFrom(type) && type != typeof(string))
|
||||||
|
return _GetValueParser(
|
||||||
|
type.GetElementType() ?? type.GenericTypeArguments.First()
|
||||||
|
);
|
||||||
|
return ParseHelper.Error<object>("Can't use 'has' on a non-list.");
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Filter<T>? From(string? filter)
|
||||||
|
{
|
||||||
|
if (filter == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IResult<Filter<T>> ret = FilterParsers.CompleteFilter.End().TryParse(filter);
|
||||||
|
if (ret.WasSuccessful)
|
||||||
|
return ret.Value;
|
||||||
|
throw new ValidationException(
|
||||||
|
$"Could not parse filter argument: {ret.Message}. Not parsed: {filter[ret.Remainder.Position..]}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (ParseException ex)
|
||||||
|
{
|
||||||
|
throw new ValidationException($"Could not parse filter argument: {ex.Message}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
245
src/Kyoo.Abstractions/Models/Utils/Identifier.cs
Normal file
245
src/Kyoo.Abstractions/Models/Utils/Identifier.cs
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A class that represent a resource. It is made to be used as a parameter in a query and not used somewhere else
|
||||||
|
/// on the application.
|
||||||
|
/// This class allow routes to be used via ether IDs or Slugs, this is suitable for every <see cref="IResource"/>.
|
||||||
|
/// </summary>
|
||||||
|
[TypeConverter(typeof(IdentifierConvertor))]
|
||||||
|
public class Identifier
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The ID of the resource or null if the slug is specified.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Guid? _id;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The slug of the resource or null if the id is specified.
|
||||||
|
/// </summary>
|
||||||
|
private readonly string? _slug;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Identifier"/> for the given id.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="id">The id of the resource.</param>
|
||||||
|
public Identifier(Guid id)
|
||||||
|
{
|
||||||
|
_id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Identifier"/> for the given slug.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="slug">The slug of the resource.</param>
|
||||||
|
public Identifier(string slug)
|
||||||
|
{
|
||||||
|
_slug = slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pattern match out of the identifier to a resource.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idFunc">The function to match the ID to a type <typeparamref name="T"/>.</param>
|
||||||
|
/// <param name="slugFunc">The function to match the slug to a type <typeparamref name="T"/>.</param>
|
||||||
|
/// <typeparam name="T">The return type that will be converted to from an ID or a slug.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// The result of the <paramref name="idFunc"/> or <paramref name="slugFunc"/> depending on the pattern.
|
||||||
|
/// </returns>
|
||||||
|
/// <example>
|
||||||
|
/// Example usage:
|
||||||
|
/// <code lang="csharp">
|
||||||
|
/// T ret = await identifier.Match(
|
||||||
|
/// id => _repository.GetOrDefault(id),
|
||||||
|
/// slug => _repository.GetOrDefault(slug)
|
||||||
|
/// );
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public T Match<T>(Func<Guid, T> idFunc, Func<string, T> slugFunc)
|
||||||
|
{
|
||||||
|
return _id.HasValue ? idFunc(_id.Value) : slugFunc(_slug!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Match a custom type to an identifier. This can be used for wrapped resources (see example for more details).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idGetter">An expression to retrieve an ID from the type <typeparamref name="T"/>.</param>
|
||||||
|
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
|
||||||
|
/// <typeparam name="T">The type to match against this identifier.</typeparam>
|
||||||
|
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
|
||||||
|
/// <example>
|
||||||
|
/// <code lang="csharp">
|
||||||
|
/// identifier.Matcher<Season>(x => x.ShowID, x => x.Show.Slug)
|
||||||
|
/// </code>
|
||||||
|
/// </example>
|
||||||
|
public Filter<T> Matcher<T>(
|
||||||
|
Expression<Func<T, Guid>> idGetter,
|
||||||
|
Expression<Func<T, string>> slugGetter
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
|
||||||
|
BinaryExpression equal = Expression.Equal(
|
||||||
|
_id.HasValue ? idGetter.Body : slugGetter.Body,
|
||||||
|
self
|
||||||
|
);
|
||||||
|
ICollection<ParameterExpression> parameters = _id.HasValue
|
||||||
|
? idGetter.Parameters
|
||||||
|
: slugGetter.Parameters;
|
||||||
|
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
|
||||||
|
return new Filter<T>.Lambda(lambda);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A matcher overload for nullable IDs. See
|
||||||
|
/// <see cref="Matcher{T}(Expression{Func{T,Guid}},Expression{Func{T,string}})"/>
|
||||||
|
/// for more details.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="idGetter">An expression to retrieve an ID from the type <typeparamref name="T"/>.</param>
|
||||||
|
/// <param name="slugGetter">An expression to retrieve a slug from the type <typeparamref name="T"/>.</param>
|
||||||
|
/// <typeparam name="T">The type to match against this identifier.</typeparam>
|
||||||
|
/// <returns>An expression to match the type <typeparamref name="T"/> to this identifier.</returns>
|
||||||
|
public Filter<T> Matcher<T>(
|
||||||
|
Expression<Func<T, Guid?>> idGetter,
|
||||||
|
Expression<Func<T, string>> slugGetter
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ConstantExpression self = Expression.Constant(_id.HasValue ? _id.Value : _slug);
|
||||||
|
BinaryExpression equal = Expression.Equal(
|
||||||
|
_id.HasValue ? idGetter.Body : slugGetter.Body,
|
||||||
|
self
|
||||||
|
);
|
||||||
|
ICollection<ParameterExpression> parameters = _id.HasValue
|
||||||
|
? idGetter.Parameters
|
||||||
|
: slugGetter.Parameters;
|
||||||
|
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(equal, parameters);
|
||||||
|
return new Filter<T>.Lambda(lambda);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return true if this <see cref="Identifier"/> match a resource.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="resource">The resource to match</param>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>true</c> if the <paramref name="resource"/> match this identifier, <c>false</c> otherwise.
|
||||||
|
/// </returns>
|
||||||
|
public bool IsSame(IResource resource)
|
||||||
|
{
|
||||||
|
return Match(id => resource.Id == id, slug => resource.Slug == slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return a filter to get this <see cref="Identifier"/> match a given resource.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type of resource to match against.</typeparam>
|
||||||
|
/// <returns>
|
||||||
|
/// <c>true</c> if the given resource match this identifier, <c>false</c> otherwise.
|
||||||
|
/// </returns>
|
||||||
|
public Filter<T> IsSame<T>()
|
||||||
|
where T : IResource
|
||||||
|
{
|
||||||
|
return _id.HasValue ? new Filter<T>.Eq("Id", _id.Value) : new Filter<T>.Eq("Slug", _slug!);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Is(Guid uid)
|
||||||
|
{
|
||||||
|
return _id.HasValue && _id.Value == uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Is(string slug)
|
||||||
|
{
|
||||||
|
return !_id.HasValue && _slug == slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Expression<Func<T, bool>> _IsSameExpression<T>()
|
||||||
|
where T : IResource
|
||||||
|
{
|
||||||
|
return _id.HasValue ? x => x.Id == _id.Value : x => x.Slug == _slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return an expression that return true if this <see cref="Identifier"/> is containing in a collection.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="listGetter">An expression to retrieve the list to check.</param>
|
||||||
|
/// <typeparam name="T">The type that contain the list to check.</typeparam>
|
||||||
|
/// <typeparam name="T2">The type of resource to check this identifier against.</typeparam>
|
||||||
|
/// <returns>An expression to check if this <see cref="Identifier"/> is contained.</returns>
|
||||||
|
public Filter<T> IsContainedIn<T, T2>(Expression<Func<T, IEnumerable<T2>?>> listGetter)
|
||||||
|
where T2 : IResource
|
||||||
|
{
|
||||||
|
MethodInfo method = typeof(Enumerable)
|
||||||
|
.GetMethods()
|
||||||
|
.Where(x => x.Name == nameof(Enumerable.Any))
|
||||||
|
.FirstOrDefault(x => x.GetParameters().Length == 2)!
|
||||||
|
.MakeGenericMethod(typeof(T2));
|
||||||
|
MethodCallExpression call = Expression.Call(
|
||||||
|
null,
|
||||||
|
method,
|
||||||
|
listGetter.Body,
|
||||||
|
_IsSameExpression<T2>()
|
||||||
|
);
|
||||||
|
Expression<Func<T, bool>> lambda = Expression.Lambda<Func<T, bool>>(
|
||||||
|
call,
|
||||||
|
listGetter.Parameters
|
||||||
|
);
|
||||||
|
return new Filter<T>.Lambda(lambda);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return _id.HasValue ? _id.Value.ToString() : _slug!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A custom <see cref="TypeConverter"/> used to convert int or strings to an <see cref="Identifier"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class IdentifierConvertor : TypeConverter
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
|
||||||
|
{
|
||||||
|
if (sourceType == typeof(Guid) || sourceType == typeof(string))
|
||||||
|
return true;
|
||||||
|
return base.CanConvertFrom(context, sourceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override object ConvertFrom(
|
||||||
|
ITypeDescriptorContext? context,
|
||||||
|
CultureInfo? culture,
|
||||||
|
object value
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (value is Guid id)
|
||||||
|
return new Identifier(id);
|
||||||
|
if (value is not string slug)
|
||||||
|
return base.ConvertFrom(context, culture, value)!;
|
||||||
|
return Guid.TryParse(slug, out id) ? new Identifier(id) : new Identifier(slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/Kyoo.Abstractions/Models/Utils/Include.cs
Normal file
109
src/Kyoo.Abstractions/Models/Utils/Include.cs
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
public class Include
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The aditional fields to include in the result.
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<Metadata> Metadatas { get; set; } = ArraySegment<Metadata>.Empty;
|
||||||
|
|
||||||
|
public abstract record Metadata(string Name);
|
||||||
|
|
||||||
|
public record SingleRelation(string Name, Type type, string RelationIdName) : Metadata(Name);
|
||||||
|
|
||||||
|
public record CustomRelation(string Name, Type type, string Sql, string? On, Type Declaring)
|
||||||
|
: Metadata(Name);
|
||||||
|
|
||||||
|
public record ProjectedRelation(string Name, string Sql) : Metadata(Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The aditional fields to include in the result.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">The type related to the new fields</typeparam>
|
||||||
|
public class Include<T> : Include
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The aditional fields names to include in the result.
|
||||||
|
/// </summary>
|
||||||
|
public ICollection<string> Fields => Metadatas.Select(x => x.Name).ToList();
|
||||||
|
|
||||||
|
public Include() { }
|
||||||
|
|
||||||
|
public Include(params string[] fields)
|
||||||
|
{
|
||||||
|
Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
|
||||||
|
Metadatas = fields
|
||||||
|
.SelectMany(key =>
|
||||||
|
{
|
||||||
|
var relations = types
|
||||||
|
.Select(x =>
|
||||||
|
x.GetProperty(
|
||||||
|
key,
|
||||||
|
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||||
|
)!
|
||||||
|
)
|
||||||
|
.Select(prop =>
|
||||||
|
(prop, attr: prop?.GetCustomAttribute<LoadableRelationAttribute>()!)
|
||||||
|
)
|
||||||
|
.Where(x => x.prop != null && x.attr != null)
|
||||||
|
.ToList();
|
||||||
|
if (!relations.Any())
|
||||||
|
throw new ValidationException($"No loadable relation with the name {key}.");
|
||||||
|
return relations
|
||||||
|
.Select(x =>
|
||||||
|
{
|
||||||
|
(PropertyInfo prop, LoadableRelationAttribute attr) = x;
|
||||||
|
|
||||||
|
if (attr.RelationID != null)
|
||||||
|
return new SingleRelation(prop.Name, prop.PropertyType, attr.RelationID)
|
||||||
|
as Metadata;
|
||||||
|
if (attr.Sql != null)
|
||||||
|
return new CustomRelation(
|
||||||
|
prop.Name,
|
||||||
|
prop.PropertyType,
|
||||||
|
attr.Sql,
|
||||||
|
attr.On,
|
||||||
|
prop.DeclaringType!
|
||||||
|
);
|
||||||
|
if (attr.Projected != null)
|
||||||
|
return new ProjectedRelation(prop.Name, attr.Projected);
|
||||||
|
throw new NotImplementedException();
|
||||||
|
})
|
||||||
|
.Distinct();
|
||||||
|
})
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Include<T> From(string? fields)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(fields))
|
||||||
|
return new Include<T>();
|
||||||
|
return new Include<T>(fields.Split(','));
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/Kyoo.Abstractions/Models/Utils/Pagination.cs
Normal file
72
src/Kyoo.Abstractions/Models/Utils/Pagination.cs
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about the pagination. How many items should be displayed and where to start.
|
||||||
|
/// </summary>
|
||||||
|
public class Pagination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The count of items to return.
|
||||||
|
/// </summary>
|
||||||
|
public int Limit { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where to start? Using the given sort.
|
||||||
|
/// </summary>
|
||||||
|
public Guid? AfterID { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Should the previous page be returned instead of the next?
|
||||||
|
/// </summary>
|
||||||
|
public bool Reverse { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Pagination"/> with default values.
|
||||||
|
/// </summary>
|
||||||
|
public Pagination()
|
||||||
|
{
|
||||||
|
Limit = 50;
|
||||||
|
AfterID = null;
|
||||||
|
Reverse = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Pagination"/> instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="count">Set the <see cref="Limit"/> value</param>
|
||||||
|
/// <param name="afterID">Set the <see cref="AfterID"/> value. If not specified, it will start from the start</param>
|
||||||
|
/// <param name="reverse">Should the previous page be returned instead of the next?</param>
|
||||||
|
public Pagination(int count, Guid? afterID = null, bool reverse = false)
|
||||||
|
{
|
||||||
|
Limit = count;
|
||||||
|
AfterID = afterID;
|
||||||
|
Reverse = reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Implicitly create a new pagination from a limit number.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="limit">Set the <see cref="Limit"/> value</param>
|
||||||
|
/// <returns>A new <see cref="Pagination"/> instance</returns>
|
||||||
|
public static implicit operator Pagination(int limit) => new(limit);
|
||||||
|
}
|
||||||
56
src/Kyoo.Abstractions/Models/Utils/RequestError.cs
Normal file
56
src/Kyoo.Abstractions/Models/Utils/RequestError.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of errors that where made in the request.
|
||||||
|
/// </summary>
|
||||||
|
public class RequestError
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list of errors that where made in the request.
|
||||||
|
/// </summary>
|
||||||
|
/// <example><c>["InvalidFilter: no field 'startYear' on a collection"]</c></example>
|
||||||
|
public string[] Errors { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="RequestError"/> with one error.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The error to specify in the response.</param>
|
||||||
|
public RequestError(string error)
|
||||||
|
{
|
||||||
|
if (error == null)
|
||||||
|
throw new ArgumentNullException(nameof(error));
|
||||||
|
Errors = new[] { error };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="RequestError"/> with multiple errors.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="errors">The errors to specify in the response.</param>
|
||||||
|
public RequestError(string[] errors)
|
||||||
|
{
|
||||||
|
if (errors == null || !errors.Any())
|
||||||
|
throw new ArgumentException("Errors must be non null and not empty", nameof(errors));
|
||||||
|
Errors = errors;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Kyoo.Abstractions/Models/Utils/SearchPagination.cs
Normal file
35
src/Kyoo.Abstractions/Models/Utils/SearchPagination.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about the pagination. How many items should be displayed and where to start.
|
||||||
|
/// </summary>
|
||||||
|
public class SearchPagination
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The count of items to return.
|
||||||
|
/// </summary>
|
||||||
|
public int Limit { get; set; } = 50;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Where to start? How many items to skip?
|
||||||
|
/// </summary>
|
||||||
|
public int? Skip { get; set; }
|
||||||
|
}
|
||||||
137
src/Kyoo.Abstractions/Models/Utils/Sort.cs
Normal file
137
src/Kyoo.Abstractions/Models/Utils/Sort.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Controllers;
|
||||||
|
|
||||||
|
public record Sort;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about how a query should be sorted. What factor should decide the sort and in which order.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="T">For witch type this sort applies</typeparam>
|
||||||
|
public record Sort<T> : Sort
|
||||||
|
where T : IQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by a specific key
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Key">The sort keys. This members will be used to sort the results.</param>
|
||||||
|
/// <param name="Desendant">
|
||||||
|
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
|
||||||
|
/// </param>
|
||||||
|
public record By(string Key, bool Desendant = false) : Sort<T>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by a specific key
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The sort keys. This members will be used to sort the results.</param>
|
||||||
|
/// <param name="desendant">
|
||||||
|
/// If this is set to true, items will be sorted in descend order else, they will be sorted in ascendant order.
|
||||||
|
/// </param>
|
||||||
|
public By(Expression<Func<T, object?>> key, bool desendant = false)
|
||||||
|
: this(Utility.GetPropertyName(key), desendant) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort by multiple keys.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="List">The list of keys to sort by.</param>
|
||||||
|
public record Conglomerate(params Sort<T>[] List) : Sort<T>;
|
||||||
|
|
||||||
|
/// <summary>Sort randomly items</summary>
|
||||||
|
public record Random(uint Seed) : Sort<T>
|
||||||
|
{
|
||||||
|
public Random()
|
||||||
|
: this(0)
|
||||||
|
{
|
||||||
|
uint seed = BitConverter.ToUInt32(
|
||||||
|
BitConverter.GetBytes(new System.Random().Next(int.MinValue, int.MaxValue)),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
Seed = seed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>The default sort method for the given type.</summary>
|
||||||
|
public record Default : Sort<T>
|
||||||
|
{
|
||||||
|
public void Deconstruct(out Sort<T> value)
|
||||||
|
{
|
||||||
|
value = (Sort<T>)T.DefaultSort;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new <see cref="Sort{T}"/> instance from a key's name (case insensitive).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sortBy">A key name with an optional order specifier. Format: "key:asc", "key:desc" or "key".</param>
|
||||||
|
/// <param name="seed">The random seed.</param>
|
||||||
|
/// <exception cref="ArgumentException">An invalid key or sort specifier as been given.</exception>
|
||||||
|
/// <returns>A <see cref="Sort{T}"/> for the given string</returns>
|
||||||
|
public static Sort<T> From(string? sortBy, uint seed)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(sortBy) || sortBy == "default")
|
||||||
|
return new Default();
|
||||||
|
if (sortBy == "random")
|
||||||
|
return new Random(seed);
|
||||||
|
if (sortBy.Contains(','))
|
||||||
|
return new Conglomerate(sortBy.Split(',').Select(x => From(x, seed)).ToArray());
|
||||||
|
|
||||||
|
if (sortBy.StartsWith("random:"))
|
||||||
|
{
|
||||||
|
if (uint.TryParse(sortBy["random:".Length..], out uint sseed))
|
||||||
|
return new Random(sseed);
|
||||||
|
throw new ValidationException("Invalid random seed specified. Expected a number.");
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = sortBy.Contains(':') ? sortBy[..sortBy.IndexOf(':')] : sortBy;
|
||||||
|
string? order = sortBy.Contains(':') ? sortBy[(sortBy.IndexOf(':') + 1)..] : null;
|
||||||
|
bool desendant = order switch
|
||||||
|
{
|
||||||
|
"desc" => true,
|
||||||
|
"asc" => false,
|
||||||
|
null => false,
|
||||||
|
_
|
||||||
|
=> throw new ValidationException(
|
||||||
|
$"The sort order, if set, should be :asc or :desc but it was :{order}."
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
Type[] types = typeof(T).GetCustomAttribute<OneOfAttribute>()?.Types ?? new[] { typeof(T) };
|
||||||
|
PropertyInfo? property = types
|
||||||
|
.Select(x =>
|
||||||
|
x.GetProperty(
|
||||||
|
key,
|
||||||
|
BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.FirstOrDefault(x => x != null);
|
||||||
|
if (property == null)
|
||||||
|
throw new ValidationException("The given sort key is not valid.");
|
||||||
|
return new By(property.Name, desendant);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Kyoo.Abstractions/Models/VideoLinks.cs
Normal file
35
src/Kyoo.Abstractions/Models/VideoLinks.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The links to see a movie or an episode.
|
||||||
|
/// </summary>
|
||||||
|
public class VideoLinks
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The direct link to the unprocessed video (pristine quality).
|
||||||
|
/// </summary>
|
||||||
|
public string Direct { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The link to an HLS master playlist containing all qualities available for this video.
|
||||||
|
/// </summary>
|
||||||
|
public string Hls { get; set; }
|
||||||
|
}
|
||||||
51
src/Kyoo.Abstractions/Utility/ExpressionParameterReplacer.cs
Normal file
51
src/Kyoo.Abstractions/Utility/ExpressionParameterReplacer.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
|
||||||
|
namespace Kyoo.Utils;
|
||||||
|
|
||||||
|
public sealed class ExpressionArgumentReplacer : ExpressionVisitor
|
||||||
|
{
|
||||||
|
private readonly Dictionary<ParameterExpression, Expression> _mapping;
|
||||||
|
|
||||||
|
public ExpressionArgumentReplacer(Dictionary<ParameterExpression, Expression> dict)
|
||||||
|
{
|
||||||
|
_mapping = dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Expression VisitParameter(ParameterExpression node)
|
||||||
|
{
|
||||||
|
if (_mapping.TryGetValue(node, out Expression? mappedArgument))
|
||||||
|
return Visit(mappedArgument);
|
||||||
|
return base.VisitParameter(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Expression ReplaceParams(
|
||||||
|
Expression expression,
|
||||||
|
IEnumerable<ParameterExpression> epxParams,
|
||||||
|
params ParameterExpression[] param
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ExpressionArgumentReplacer replacer =
|
||||||
|
new(epxParams.Zip(param).ToDictionary(x => x.First, x => x.Second as Expression));
|
||||||
|
return replacer.Visit(expression);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/Kyoo.Abstractions/Utility/JsonKindResolver.cs
Normal file
78
src/Kyoo.Abstractions/Utility/JsonKindResolver.cs
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization.Metadata;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
using static System.Text.Json.JsonNamingPolicy;
|
||||||
|
|
||||||
|
namespace Kyoo.Utils;
|
||||||
|
|
||||||
|
public class JsonKindResolver : DefaultJsonTypeInfoResolver
|
||||||
|
{
|
||||||
|
public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);
|
||||||
|
|
||||||
|
if (jsonTypeInfo.Type.GetCustomAttribute<OneOfAttribute>() != null)
|
||||||
|
{
|
||||||
|
jsonTypeInfo.PolymorphismOptions = new()
|
||||||
|
{
|
||||||
|
TypeDiscriminatorPropertyName = "kind",
|
||||||
|
IgnoreUnrecognizedTypeDiscriminators = true,
|
||||||
|
DerivedTypes = { },
|
||||||
|
};
|
||||||
|
IEnumerable<Type> derived = AppDomain
|
||||||
|
.CurrentDomain.GetAssemblies()
|
||||||
|
.SelectMany(s => s.GetTypes())
|
||||||
|
.Where(p => type.IsAssignableFrom(p) && p.IsClass);
|
||||||
|
foreach (Type der in derived)
|
||||||
|
{
|
||||||
|
jsonTypeInfo.PolymorphismOptions.DerivedTypes.Add(
|
||||||
|
new JsonDerivedType(der, CamelCase.ConvertName(der.Name))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (
|
||||||
|
jsonTypeInfo.Type.IsAssignableTo(typeof(IResource))
|
||||||
|
&& jsonTypeInfo.Properties.All(x => x.Name != "kind")
|
||||||
|
)
|
||||||
|
{
|
||||||
|
jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
|
||||||
|
{
|
||||||
|
TypeDiscriminatorPropertyName = "kind",
|
||||||
|
IgnoreUnrecognizedTypeDiscriminators = true,
|
||||||
|
DerivedTypes =
|
||||||
|
{
|
||||||
|
new JsonDerivedType(
|
||||||
|
jsonTypeInfo.Type,
|
||||||
|
CamelCase.ConvertName(jsonTypeInfo.Type.Name)
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonTypeInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
212
src/Kyoo.Abstractions/Utility/Utility.cs
Normal file
212
src/Kyoo.Abstractions/Utility/Utility.cs
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Linq.Expressions;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace Kyoo.Utils;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A set of utility functions that can be used everywhere.
|
||||||
|
/// </summary>
|
||||||
|
public static class Utility
|
||||||
|
{
|
||||||
|
public static readonly JsonSerializerOptions JsonOptions =
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
TypeInfoResolver = new JsonKindResolver(),
|
||||||
|
Converters = { new JsonStringEnumConverter() },
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a string to snake case. Stollen from
|
||||||
|
/// https://github.com/efcore/EFCore.NamingConventions/blob/main/EFCore.NamingConventions/Internal/SnakeCaseNameRewriter.cs
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="name">The string to convert.</param>
|
||||||
|
/// <returns>The string in snake case</returns>
|
||||||
|
public static string ToSnakeCase(this string name)
|
||||||
|
{
|
||||||
|
StringBuilder builder = new(name.Length + Math.Min(2, name.Length / 5));
|
||||||
|
UnicodeCategory? previousCategory = default;
|
||||||
|
|
||||||
|
for (int currentIndex = 0; currentIndex < name.Length; currentIndex++)
|
||||||
|
{
|
||||||
|
char currentChar = name[currentIndex];
|
||||||
|
if (currentChar == '_')
|
||||||
|
{
|
||||||
|
builder.Append('_');
|
||||||
|
previousCategory = null;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UnicodeCategory currentCategory = char.GetUnicodeCategory(currentChar);
|
||||||
|
switch (currentCategory)
|
||||||
|
{
|
||||||
|
case UnicodeCategory.UppercaseLetter:
|
||||||
|
case UnicodeCategory.TitlecaseLetter:
|
||||||
|
if (
|
||||||
|
previousCategory == UnicodeCategory.SpaceSeparator
|
||||||
|
|| previousCategory == UnicodeCategory.LowercaseLetter
|
||||||
|
|| (
|
||||||
|
previousCategory != UnicodeCategory.DecimalDigitNumber
|
||||||
|
&& previousCategory != null
|
||||||
|
&& currentIndex > 0
|
||||||
|
&& currentIndex + 1 < name.Length
|
||||||
|
&& char.IsLower(name[currentIndex + 1])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
builder.Append('_');
|
||||||
|
}
|
||||||
|
|
||||||
|
currentChar = char.ToLowerInvariant(currentChar);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case UnicodeCategory.LowercaseLetter:
|
||||||
|
case UnicodeCategory.DecimalDigitNumber:
|
||||||
|
if (previousCategory == UnicodeCategory.SpaceSeparator)
|
||||||
|
{
|
||||||
|
builder.Append('_');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (previousCategory != null)
|
||||||
|
{
|
||||||
|
previousCategory = UnicodeCategory.SpaceSeparator;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(currentChar);
|
||||||
|
previousCategory = currentCategory;
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Is the lambda expression a member (like x => x.Body).
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ex">The expression that should be checked</param>
|
||||||
|
/// <returns>True if the expression is a member, false otherwise</returns>
|
||||||
|
public static bool IsPropertyExpression(LambdaExpression ex)
|
||||||
|
{
|
||||||
|
return ex.Body is MemberExpression
|
||||||
|
|| (
|
||||||
|
ex.Body.NodeType == ExpressionType.Convert
|
||||||
|
&& ((UnaryExpression)ex.Body).Operand is MemberExpression
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the name of a property. Useful for selectors as members ex: Load(x => x.Shows)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ex">The expression</param>
|
||||||
|
/// <returns>The name of the expression</returns>
|
||||||
|
/// <exception cref="ArgumentException">If the expression is not a property, ArgumentException is thrown.</exception>
|
||||||
|
public static string GetPropertyName(LambdaExpression ex)
|
||||||
|
{
|
||||||
|
if (!IsPropertyExpression(ex))
|
||||||
|
throw new ArgumentException($"{ex} is not a property expression.");
|
||||||
|
MemberExpression? member =
|
||||||
|
ex.Body.NodeType == ExpressionType.Convert
|
||||||
|
? ((UnaryExpression)ex.Body).Operand as MemberExpression
|
||||||
|
: ex.Body as MemberExpression;
|
||||||
|
return member!.Member.Name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Slugify a string (Replace spaces by -, Uniformize accents)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="str">The string to slugify</param>
|
||||||
|
/// <returns>The slug version of the given string</returns>
|
||||||
|
public static string ToSlug(string str)
|
||||||
|
{
|
||||||
|
str = str.ToLowerInvariant();
|
||||||
|
|
||||||
|
string normalizedString = str.Normalize(NormalizationForm.FormD);
|
||||||
|
StringBuilder stringBuilder = new();
|
||||||
|
foreach (char c in normalizedString)
|
||||||
|
{
|
||||||
|
UnicodeCategory unicodeCategory = CharUnicodeInfo.GetUnicodeCategory(c);
|
||||||
|
if (unicodeCategory != UnicodeCategory.NonSpacingMark)
|
||||||
|
stringBuilder.Append(c);
|
||||||
|
}
|
||||||
|
str = stringBuilder.ToString().Normalize(NormalizationForm.FormC);
|
||||||
|
|
||||||
|
str = Regex.Replace(str, @"\s", "-", RegexOptions.Compiled);
|
||||||
|
str = Regex.Replace(str, @"[^\w\s\p{Pd}]", string.Empty, RegexOptions.Compiled);
|
||||||
|
str = str.Trim('-', '_');
|
||||||
|
str = Regex.Replace(str, @"([-_]){2,}", "$1", RegexOptions.Compiled);
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Return every <see cref="Type"/> in the inheritance tree of the parameter (interfaces are not returned)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="self">The starting type</param>
|
||||||
|
/// <returns>A list of types</returns>
|
||||||
|
public static IEnumerable<Type> GetInheritanceTree(this Type self)
|
||||||
|
{
|
||||||
|
for (Type? type = self; type != null; type = type.BaseType)
|
||||||
|
yield return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the generic definition of <paramref name="genericType"/>.
|
||||||
|
/// For example, calling this function with List<string> and typeof(IEnumerable<>) will return IEnumerable<string>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="type">The type to check</param>
|
||||||
|
/// <param name="genericType">The generic type to check against (Only generic types are supported like typeof(IEnumerable<>).</param>
|
||||||
|
/// <returns>The generic definition of genericType that type inherit or null if type does not implement the generic type.</returns>
|
||||||
|
/// <exception cref="ArgumentException"><paramref name="genericType"/> must be a generic type</exception>
|
||||||
|
public static Type? GetGenericDefinition(Type type, Type genericType)
|
||||||
|
{
|
||||||
|
if (!genericType.IsGenericType)
|
||||||
|
throw new ArgumentException($"{nameof(genericType)} is not a generic type.");
|
||||||
|
|
||||||
|
IEnumerable<Type> types = genericType.IsInterface
|
||||||
|
? type.GetInterfaces()
|
||||||
|
: type.GetInheritanceTree();
|
||||||
|
return types
|
||||||
|
.Prepend(type)
|
||||||
|
.FirstOrDefault(x => x.IsGenericType && x.GetGenericTypeDefinition() == genericType);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert a dictionary to a query string.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="query">The list of query parameters.</param>
|
||||||
|
/// <returns>A valid query string with all items in the dictionary.</returns>
|
||||||
|
public static string ToQueryString(this Dictionary<string, string> query)
|
||||||
|
{
|
||||||
|
if (!query.Any())
|
||||||
|
return string.Empty;
|
||||||
|
return "?" + string.Join('&', query.Select(x => $"{x.Key}={x.Value}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
47
src/Kyoo.Abstractions/Utility/Wrapper.cs
Normal file
47
src/Kyoo.Abstractions/Utility/Wrapper.cs
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Data;
|
||||||
|
using Dapper;
|
||||||
|
|
||||||
|
namespace Kyoo.Utils;
|
||||||
|
|
||||||
|
// Only used due to https://github.com/DapperLib/Dapper/issues/332
|
||||||
|
public class Wrapper
|
||||||
|
{
|
||||||
|
public object Value { get; set; }
|
||||||
|
|
||||||
|
public Wrapper(object value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class Handler : SqlMapper.TypeHandler<Wrapper>
|
||||||
|
{
|
||||||
|
public override Wrapper? Parse(object value)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException("Wrapper should only be used to write");
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void SetValue(IDbDataParameter parameter, Wrapper? value)
|
||||||
|
{
|
||||||
|
parameter.Value = value?.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Attributes;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables the action if the specified environment variable is set to true.
|
||||||
|
/// </summary>
|
||||||
|
public class DisableOnEnvVarAttribute(string varName) : Attribute, IResourceFilter
|
||||||
|
{
|
||||||
|
public void OnResourceExecuting(ResourceExecutingContext context)
|
||||||
|
{
|
||||||
|
var config = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
|
||||||
|
|
||||||
|
if (config.GetValue(varName, false))
|
||||||
|
context.Result = new Microsoft.AspNetCore.Mvc.NotFoundResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnResourceExecuted(ResourceExecutedContext context) { }
|
||||||
|
}
|
||||||
165
src/Kyoo.Authentication/AuthenticationModule.cs
Normal file
165
src/Kyoo.Authentication/AuthenticationModule.cs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Authentication.Models;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication;
|
||||||
|
|
||||||
|
public static class AuthenticationModule
|
||||||
|
{
|
||||||
|
public static void ConfigureAuthentication(this WebApplicationBuilder builder)
|
||||||
|
{
|
||||||
|
PermissionOption options =
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Default = builder
|
||||||
|
.Configuration.GetValue("UNLOGGED_PERMISSIONS", "")!
|
||||||
|
.Split(',')
|
||||||
|
.Where(x => x.Length > 0)
|
||||||
|
.ToArray(),
|
||||||
|
NewUser = builder
|
||||||
|
.Configuration.GetValue("DEFAULT_PERMISSIONS", "overall.read,overall.play")!
|
||||||
|
.Split(','),
|
||||||
|
RequireVerification = builder.Configuration.GetValue(
|
||||||
|
"REQUIRE_ACCOUNT_VERIFICATION",
|
||||||
|
true
|
||||||
|
),
|
||||||
|
PublicUrl =
|
||||||
|
builder.Configuration.GetValue<string?>("PUBLIC_URL")
|
||||||
|
?? "http://localhost:8901",
|
||||||
|
ApiKeys = builder.Configuration.GetValue("KYOO_APIKEYS", string.Empty)!.Split(','),
|
||||||
|
OIDC = builder
|
||||||
|
.Configuration.AsEnumerable()
|
||||||
|
.Where((pair) => pair.Key.StartsWith("OIDC_"))
|
||||||
|
.Aggregate(
|
||||||
|
new Dictionary<string, OidcProvider>(),
|
||||||
|
(acc, val) =>
|
||||||
|
{
|
||||||
|
if (val.Value is null)
|
||||||
|
return acc;
|
||||||
|
if (val.Key.Split("_") is not ["OIDC", string provider, string key])
|
||||||
|
{
|
||||||
|
Log.Error("Invalid oidc config value: {Key}", val.Key);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
provider = provider.ToLowerInvariant();
|
||||||
|
key = key.ToLowerInvariant();
|
||||||
|
|
||||||
|
if (!acc.ContainsKey(provider))
|
||||||
|
acc.Add(provider, new(provider));
|
||||||
|
switch (key)
|
||||||
|
{
|
||||||
|
case "clientid":
|
||||||
|
acc[provider].ClientId = val.Value;
|
||||||
|
break;
|
||||||
|
case "secret":
|
||||||
|
acc[provider].Secret = val.Value;
|
||||||
|
break;
|
||||||
|
case "scope":
|
||||||
|
acc[provider].Scope = val.Value;
|
||||||
|
break;
|
||||||
|
case "authorization":
|
||||||
|
acc[provider].AuthorizationUrl = val.Value;
|
||||||
|
break;
|
||||||
|
case "token":
|
||||||
|
acc[provider].TokenUrl = val.Value;
|
||||||
|
break;
|
||||||
|
case "userinfo":
|
||||||
|
case "profile":
|
||||||
|
acc[provider].ProfileUrl = val.Value;
|
||||||
|
break;
|
||||||
|
case "name":
|
||||||
|
acc[provider].DisplayName = val.Value;
|
||||||
|
break;
|
||||||
|
case "logo":
|
||||||
|
acc[provider].LogoUrl = val.Value;
|
||||||
|
break;
|
||||||
|
case "clientauthmethod":
|
||||||
|
case "authmethod":
|
||||||
|
case "auth":
|
||||||
|
case "method":
|
||||||
|
if (!Enum.TryParse(val.Value, out AuthMethod method))
|
||||||
|
{
|
||||||
|
Log.Error(
|
||||||
|
"Invalid AuthMethod value: {AuthMethod}. Ignoring.",
|
||||||
|
val.Value
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
acc[provider].ClientAuthMethod = method;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.Error("Invalid oidc config value: {Key}", key);
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
builder.Services.AddSingleton(options);
|
||||||
|
|
||||||
|
byte[] secret = builder.Configuration.GetValue<byte[]>("AUTHENTICATION_SECRET")!;
|
||||||
|
builder.Services.AddSingleton(new AuthenticationOption() { Secret = secret });
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
|
||||||
|
.AddJwtBearer(options =>
|
||||||
|
{
|
||||||
|
options.Events = new()
|
||||||
|
{
|
||||||
|
OnMessageReceived = (ctx) =>
|
||||||
|
{
|
||||||
|
string prefix = "Bearer ";
|
||||||
|
if (
|
||||||
|
ctx.Request.Headers.TryGetValue("Authorization", out StringValues val)
|
||||||
|
&& val.ToString() is string auth
|
||||||
|
&& auth.StartsWith(prefix)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ctx.Token ??= auth[prefix.Length..];
|
||||||
|
}
|
||||||
|
ctx.Token ??= ctx.Request.Cookies["X-Bearer"];
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = false,
|
||||||
|
ValidateAudience = false,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
IssuerSigningKey = new SymmetricSecurityKey(secret)
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddSingleton<IPermissionValidator, PermissionValidator>();
|
||||||
|
builder.Services.AddSingleton<ITokenController, TokenController>();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
src/Kyoo.Authentication/Controllers/ITokenController.cs
Normal file
53
src/Kyoo.Authentication/Controllers/ITokenController.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The service that controls jwt creation and validation.
|
||||||
|
/// </summary>
|
||||||
|
public interface ITokenController
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new access token for the given user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user to create a token for.</param>
|
||||||
|
/// <param name="expireIn">When this token will expire.</param>
|
||||||
|
/// <returns>A new, valid access token.</returns>
|
||||||
|
string CreateAccessToken(User user, out TimeSpan expireIn);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new refresh token for the given user.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="user">The user to create a token for.</param>
|
||||||
|
/// <returns>A new, valid refresh token.</returns>
|
||||||
|
Task<string> CreateRefreshToken(User user);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if the given refresh token is valid and if it is, retrieve the id of the user this token belongs to.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="refreshToken">The refresh token to validate.</param>
|
||||||
|
/// <exception cref="SecurityTokenException">The given refresh token is not valid.</exception>
|
||||||
|
/// <returns>The id of the token's user.</returns>
|
||||||
|
Guid GetRefreshTokenUserID(string refreshToken);
|
||||||
|
}
|
||||||
143
src/Kyoo.Authentication/Controllers/OidcController.cs
Normal file
143
src/Kyoo.Authentication/Controllers/OidcController.cs
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Authentication.Models;
|
||||||
|
using Kyoo.Authentication.Models.DTO;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication;
|
||||||
|
|
||||||
|
public class OidcController(
|
||||||
|
IUserRepository users,
|
||||||
|
IHttpClientFactory clientFactory,
|
||||||
|
PermissionOption options
|
||||||
|
)
|
||||||
|
{
|
||||||
|
private async Task<(User, ExternalToken)> _TranslateCode(string provider, string code)
|
||||||
|
{
|
||||||
|
OidcProvider prov = options.OIDC[provider];
|
||||||
|
|
||||||
|
HttpClient client = clientFactory.CreateClient();
|
||||||
|
|
||||||
|
Dictionary<string, string> data =
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
["code"] = code,
|
||||||
|
["redirect_uri"] = $"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
||||||
|
["grant_type"] = "authorization_code",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (prov.ClientAuthMethod == AuthMethod.ClientSecretBasic)
|
||||||
|
{
|
||||||
|
string auth = Convert.ToBase64String(
|
||||||
|
Encoding.UTF8.GetBytes($"{prov.ClientId}:{prov.Secret}")
|
||||||
|
);
|
||||||
|
client.DefaultRequestHeaders.Add("Authorization", $"Basic {auth}");
|
||||||
|
}
|
||||||
|
else if (prov.ClientAuthMethod == AuthMethod.ClientSecretPost)
|
||||||
|
{
|
||||||
|
data["client_id"] = prov.ClientId;
|
||||||
|
data["client_secret"] = prov.Secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponseMessage resp = prov.TokenUseJsonBody
|
||||||
|
? await client.PostAsJsonAsync(prov.TokenUrl, data)
|
||||||
|
: await client.PostAsync(prov.TokenUrl, new FormUrlEncodedContent(data));
|
||||||
|
if (!resp.IsSuccessStatusCode)
|
||||||
|
throw new ValidationException(
|
||||||
|
$"Invalid code or configuration. {resp.StatusCode}: {await resp.Content.ReadAsStringAsync()}"
|
||||||
|
);
|
||||||
|
JwtToken? token = await resp.Content.ReadFromJsonAsync<JwtToken>();
|
||||||
|
if (token is null)
|
||||||
|
throw new ValidationException("Could not retrive token.");
|
||||||
|
|
||||||
|
client.DefaultRequestHeaders.Remove("Authorization");
|
||||||
|
client.DefaultRequestHeaders.Add("Authorization", $"{token.TokenType} {token.AccessToken}");
|
||||||
|
Dictionary<string, string>? extraHeaders = prov.GetExtraHeaders?.Invoke(prov);
|
||||||
|
if (extraHeaders is not null)
|
||||||
|
{
|
||||||
|
foreach ((string key, string value) in extraHeaders)
|
||||||
|
client.DefaultRequestHeaders.Add(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
JwtProfile? profile = await client.GetFromJsonAsync<JwtProfile>(prov.ProfileUrl);
|
||||||
|
if (profile is null || profile.Sub is null)
|
||||||
|
throw new ValidationException(
|
||||||
|
$"Missing sub on user object. Got: {JsonSerializer.Serialize(profile)}"
|
||||||
|
);
|
||||||
|
ExternalToken extToken =
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = profile.Sub,
|
||||||
|
Token = token,
|
||||||
|
ProfileUrl = prov.GetProfileUrl?.Invoke(profile),
|
||||||
|
};
|
||||||
|
User newUser = new();
|
||||||
|
if (profile.Email is not null)
|
||||||
|
newUser.Email = profile.Email;
|
||||||
|
if (profile.Username is null)
|
||||||
|
{
|
||||||
|
throw new ValidationException(
|
||||||
|
$"Could not find a username for the user. You may need to add more scopes. Fields: {string.Join(',', profile.Extra)}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
extToken.Username = profile.Username;
|
||||||
|
newUser.Username = profile.Username;
|
||||||
|
newUser.Slug = Utils.Utility.ToSlug(newUser.Username);
|
||||||
|
newUser.ExternalId.Add(provider, extToken);
|
||||||
|
return (newUser, extToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> LoginViaCode(string provider, string code)
|
||||||
|
{
|
||||||
|
(User newUser, ExternalToken extToken) = await _TranslateCode(provider, code);
|
||||||
|
User? user = await users.GetByExternalId(provider, extToken.Id);
|
||||||
|
if (user == null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
user = await users.Create(newUser);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
throw new ValidationException(
|
||||||
|
"A user already exists with the same username. If this is you, login via username and then link your account."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<User> LinkAccountOrLogin(Guid userId, string provider, string code)
|
||||||
|
{
|
||||||
|
(_, ExternalToken extToken) = await _TranslateCode(provider, code);
|
||||||
|
User? user = await users.GetByExternalId(provider, extToken.Id);
|
||||||
|
if (user != null)
|
||||||
|
return user;
|
||||||
|
return await users.AddExternalToken(userId, provider, extToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
284
src/Kyoo.Authentication/Controllers/PermissionValidator.cs
Normal file
284
src/Kyoo.Authentication/Controllers/PermissionValidator.cs
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models.Permissions;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Kyoo.Authentication.Models;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Filters;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A permission validator to validate permission with user Permission array
|
||||||
|
/// or the default array from the configurations if the user is not logged.
|
||||||
|
/// </summary>
|
||||||
|
public class PermissionValidator : IPermissionValidator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The permissions options to retrieve default permissions.
|
||||||
|
/// </summary>
|
||||||
|
private readonly PermissionOption _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new factory with the given options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="options">The option containing default values.</param>
|
||||||
|
public PermissionValidator(PermissionOption options)
|
||||||
|
{
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IFilterMetadata Create(PermissionAttribute attribute)
|
||||||
|
{
|
||||||
|
return new PermissionValidatorFilter(
|
||||||
|
attribute.Type,
|
||||||
|
attribute.Kind,
|
||||||
|
attribute.Group,
|
||||||
|
_options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IFilterMetadata Create(PartialPermissionAttribute attribute)
|
||||||
|
{
|
||||||
|
return new PermissionValidatorFilter(
|
||||||
|
((object?)attribute.Type ?? attribute.Kind)!,
|
||||||
|
attribute.Group,
|
||||||
|
_options
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The authorization filter used by <see cref="PermissionValidator"/>.
|
||||||
|
/// </summary>
|
||||||
|
private class PermissionValidatorFilter : IAsyncAuthorizationFilter
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The permission to validate.
|
||||||
|
/// </summary>
|
||||||
|
private readonly string? _permission;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The kind of permission needed.
|
||||||
|
/// </summary>
|
||||||
|
private readonly Kind? _kind;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The group of he permission.
|
||||||
|
/// </summary>
|
||||||
|
private Group _group;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The permissions options to retrieve default permissions.
|
||||||
|
/// </summary>
|
||||||
|
private readonly PermissionOption _options;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new permission validator with the given options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="permission">The permission to validate.</param>
|
||||||
|
/// <param name="kind">The kind of permission needed.</param>
|
||||||
|
/// <param name="group">The group of the permission.</param>
|
||||||
|
/// <param name="options">The option containing default values.</param>
|
||||||
|
public PermissionValidatorFilter(
|
||||||
|
string permission,
|
||||||
|
Kind kind,
|
||||||
|
Group group,
|
||||||
|
PermissionOption options
|
||||||
|
)
|
||||||
|
{
|
||||||
|
_permission = permission;
|
||||||
|
_kind = kind;
|
||||||
|
_group = group;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new permission validator with the given options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="partialInfo">The partial permission to validate.</param>
|
||||||
|
/// <param name="group">The group of the permission.</param>
|
||||||
|
/// <param name="options">The option containing default values.</param>
|
||||||
|
public PermissionValidatorFilter(object partialInfo, Group? group, PermissionOption options)
|
||||||
|
{
|
||||||
|
switch (partialInfo)
|
||||||
|
{
|
||||||
|
case Kind kind:
|
||||||
|
_kind = kind;
|
||||||
|
break;
|
||||||
|
case string perm:
|
||||||
|
_permission = perm;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException(
|
||||||
|
$"{nameof(partialInfo)} can only be a permission string or a kind."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group is not null and not Group.None)
|
||||||
|
_group = group.Value;
|
||||||
|
_options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
|
||||||
|
{
|
||||||
|
string? permission = _permission;
|
||||||
|
Kind? kind = _kind;
|
||||||
|
|
||||||
|
if (permission == null || kind == null)
|
||||||
|
{
|
||||||
|
if (context.HttpContext.Items["PermissionGroup"] is Group group and not Group.None)
|
||||||
|
_group = group;
|
||||||
|
else if (_group == Group.None)
|
||||||
|
_group = Group.Overall;
|
||||||
|
else
|
||||||
|
context.HttpContext.Items["PermissionGroup"] = _group;
|
||||||
|
|
||||||
|
switch (context.HttpContext.Items["PermissionType"])
|
||||||
|
{
|
||||||
|
case string perm:
|
||||||
|
permission = perm;
|
||||||
|
break;
|
||||||
|
case Kind kin:
|
||||||
|
kind = kin;
|
||||||
|
break;
|
||||||
|
case null when kind != null:
|
||||||
|
context.HttpContext.Items["PermissionType"] = kind;
|
||||||
|
return;
|
||||||
|
case null when permission != null:
|
||||||
|
context.HttpContext.Items["PermissionType"] = permission;
|
||||||
|
return;
|
||||||
|
default:
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Multiple non-matching partial permission attribute "
|
||||||
|
+ "are not supported."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (permission == null || kind == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"The permission type or kind is still missing after two partial "
|
||||||
|
+ "permission attributes, this is unsupported."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string permStr = $"{permission.ToLower()}.{kind.ToString()!.ToLower()}";
|
||||||
|
string overallStr = $"{_group.ToString().ToLower()}.{kind.ToString()!.ToLower()}";
|
||||||
|
AuthenticateResult res = _ApiKeyCheck(context);
|
||||||
|
if (res.None)
|
||||||
|
res = await _JwtCheck(context);
|
||||||
|
|
||||||
|
if (res.Succeeded)
|
||||||
|
{
|
||||||
|
ICollection<string> permissions = res.Principal.GetPermissions();
|
||||||
|
if (permissions.All(x => x != permStr && x != overallStr))
|
||||||
|
context.Result = _ErrorResult(
|
||||||
|
$"Missing permission {permStr} or {overallStr}",
|
||||||
|
StatusCodes.Status403Forbidden
|
||||||
|
);
|
||||||
|
}
|
||||||
|
else if (res.None)
|
||||||
|
{
|
||||||
|
ICollection<string> permissions = _options.Default ?? Array.Empty<string>();
|
||||||
|
if (permissions.All(x => x != permStr && x != overallStr))
|
||||||
|
{
|
||||||
|
context.Result = _ErrorResult(
|
||||||
|
$"Unlogged user does not have permission {permStr} or {overallStr}",
|
||||||
|
StatusCodes.Status401Unauthorized
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (res.Failure != null)
|
||||||
|
context.Result = _ErrorResult(res.Failure.Message, StatusCodes.Status403Forbidden);
|
||||||
|
else
|
||||||
|
context.Result = _ErrorResult(
|
||||||
|
"Authentication panic",
|
||||||
|
StatusCodes.Status500InternalServerError
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private AuthenticateResult _ApiKeyCheck(ActionContext context)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
!context.HttpContext.Request.Headers.TryGetValue(
|
||||||
|
"X-API-Key",
|
||||||
|
out StringValues apiKey
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return AuthenticateResult.NoResult();
|
||||||
|
if (!_options.ApiKeys.Contains<string>(apiKey!))
|
||||||
|
return AuthenticateResult.Fail("Invalid API-Key.");
|
||||||
|
return AuthenticateResult.Success(
|
||||||
|
new AuthenticationTicket(
|
||||||
|
new ClaimsPrincipal(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
new ClaimsIdentity(
|
||||||
|
new[]
|
||||||
|
{
|
||||||
|
// TODO: Make permission configurable, for now every APIKEY as all permissions.
|
||||||
|
new Claim(
|
||||||
|
Claims.Permissions,
|
||||||
|
string.Join(',', PermissionOption.Admin)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
"apikey"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<AuthenticateResult> _JwtCheck(ActionContext context)
|
||||||
|
{
|
||||||
|
AuthenticateResult ret = await context.HttpContext.AuthenticateAsync(
|
||||||
|
JwtBearerDefaults.AuthenticationScheme
|
||||||
|
);
|
||||||
|
// Change the failure message to make the API nice to use.
|
||||||
|
if (ret.Failure != null)
|
||||||
|
return AuthenticateResult.Fail("Invalid JWT token. The token may have expired.");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new action result with the given error message and error code.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="error">The error message.</param>
|
||||||
|
/// <param name="code">The status code of the error.</param>
|
||||||
|
/// <returns>The resulting error action.</returns>
|
||||||
|
private static IActionResult _ErrorResult(string error, int code)
|
||||||
|
{
|
||||||
|
return new ObjectResult(new RequestError(error)) { StatusCode = code };
|
||||||
|
}
|
||||||
|
}
|
||||||
116
src/Kyoo.Authentication/Controllers/TokenController.cs
Normal file
116
src/Kyoo.Authentication/Controllers/TokenController.cs
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Security.Claims;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Authentication.Models;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication;
|
||||||
|
|
||||||
|
public class TokenController(AuthenticationOption options) : ITokenController
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string CreateAccessToken(User user, out TimeSpan expireIn)
|
||||||
|
{
|
||||||
|
expireIn = new TimeSpan(1, 0, 0);
|
||||||
|
|
||||||
|
SymmetricSecurityKey key = new(options.Secret);
|
||||||
|
SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
|
||||||
|
string permissions =
|
||||||
|
user.Permissions != null ? string.Join(',', user.Permissions) : string.Empty;
|
||||||
|
List<Claim> claims =
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
new Claim(Claims.Id, user.Id.ToString()),
|
||||||
|
new Claim(Claims.Name, user.Username),
|
||||||
|
new Claim(Claims.Permissions, permissions),
|
||||||
|
new Claim(Claims.Type, "access")
|
||||||
|
};
|
||||||
|
if (user.Email != null)
|
||||||
|
claims.Add(new Claim(Claims.Email, user.Email));
|
||||||
|
JwtSecurityToken token =
|
||||||
|
new(
|
||||||
|
signingCredentials: credential,
|
||||||
|
claims: claims,
|
||||||
|
expires: DateTime.UtcNow.Add(expireIn)
|
||||||
|
);
|
||||||
|
return new JwtSecurityTokenHandler().WriteToken(token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<string> CreateRefreshToken(User user)
|
||||||
|
{
|
||||||
|
SymmetricSecurityKey key = new(options.Secret);
|
||||||
|
SigningCredentials credential = new(key, SecurityAlgorithms.HmacSha256Signature);
|
||||||
|
JwtSecurityToken token =
|
||||||
|
new(
|
||||||
|
signingCredentials: credential,
|
||||||
|
claims: new[]
|
||||||
|
{
|
||||||
|
new Claim(Claims.Id, user.Id.ToString()),
|
||||||
|
new Claim(Claims.Guid, Guid.NewGuid().ToString()),
|
||||||
|
new Claim(Claims.Type, "refresh")
|
||||||
|
},
|
||||||
|
expires: DateTime.UtcNow.AddYears(1)
|
||||||
|
);
|
||||||
|
// TODO: refresh keys are unique (thanks to the guid) but we could store them in DB to invalidate them if requested by the user.
|
||||||
|
return Task.FromResult(new JwtSecurityTokenHandler().WriteToken(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Guid GetRefreshTokenUserID(string refreshToken)
|
||||||
|
{
|
||||||
|
SymmetricSecurityKey key = new(options.Secret);
|
||||||
|
JwtSecurityTokenHandler tokenHandler = new();
|
||||||
|
ClaimsPrincipal principal;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
principal = tokenHandler.ValidateToken(
|
||||||
|
refreshToken,
|
||||||
|
new TokenValidationParameters
|
||||||
|
{
|
||||||
|
ValidateIssuer = false,
|
||||||
|
ValidateAudience = false,
|
||||||
|
ValidateIssuerSigningKey = true,
|
||||||
|
ValidateLifetime = true,
|
||||||
|
IssuerSigningKey = key
|
||||||
|
},
|
||||||
|
out SecurityToken _
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new SecurityTokenException("Invalid refresh token");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (principal.Claims.First(x => x.Type == Claims.Type).Value != "refresh")
|
||||||
|
throw new SecurityTokenException(
|
||||||
|
"Invalid token type. The token should be a refresh token."
|
||||||
|
);
|
||||||
|
Claim identifier = principal.Claims.First(x => x.Type == Claims.Id);
|
||||||
|
if (Guid.TryParse(identifier.Value, out Guid id))
|
||||||
|
return id;
|
||||||
|
throw new SecurityTokenException("Token not associated to any user.");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
src/Kyoo.Authentication/Kyoo.Authentication.csproj
Normal file
11
src/Kyoo.Authentication/Kyoo.Authentication.csproj
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.21" />
|
||||||
|
<PackageReference Include="BCrypt.Net-Next" Version="4.0.3" />
|
||||||
|
<PackageReference Include="Serilog" Version="4.3.0" />
|
||||||
|
|
||||||
|
<ProjectReference Include="../Kyoo.Abstractions/Kyoo.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
77
src/Kyoo.Authentication/Models/DTO/JwtProfile.cs
Normal file
77
src/Kyoo.Authentication/Models/DTO/JwtProfile.cs
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Nodes;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models.DTO;
|
||||||
|
|
||||||
|
public class JwtProfile
|
||||||
|
{
|
||||||
|
public string? Sub { get; set; }
|
||||||
|
public string? Uid
|
||||||
|
{
|
||||||
|
set => Sub ??= value;
|
||||||
|
}
|
||||||
|
public string? Id
|
||||||
|
{
|
||||||
|
set => Sub ??= value;
|
||||||
|
}
|
||||||
|
public string? Guid
|
||||||
|
{
|
||||||
|
set => Sub ??= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Username { get; set; }
|
||||||
|
public string? Name
|
||||||
|
{
|
||||||
|
set => Username ??= value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string? Email { get; set; }
|
||||||
|
|
||||||
|
public JsonObject? Account
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
return;
|
||||||
|
// simkl store their ids there.
|
||||||
|
Sub ??= value["id"]?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public JsonObject? User
|
||||||
|
{
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
return;
|
||||||
|
// trakt store their name there (they also store name but that's not the same).
|
||||||
|
Username ??= value["username"]?.ToString();
|
||||||
|
// simkl store their name there.
|
||||||
|
Username ??= value["name"]?.ToString();
|
||||||
|
|
||||||
|
Sub ??= value["ids"]?["uuid"]?.ToString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[JsonExtensionData]
|
||||||
|
public Dictionary<string, object> Extra { get; set; }
|
||||||
|
}
|
||||||
46
src/Kyoo.Authentication/Models/DTO/LoginRequest.cs
Normal file
46
src/Kyoo.Authentication/Models/DTO/LoginRequest.cs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models.DTO;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A model only used on login requests.
|
||||||
|
/// </summary>
|
||||||
|
public class LoginRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user's username.
|
||||||
|
/// </summary>
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's password.
|
||||||
|
/// </summary>
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="LoginRequest"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="username">The user's username.</param>
|
||||||
|
/// <param name="password">The user's password.</param>
|
||||||
|
public LoginRequest(string username, string password)
|
||||||
|
{
|
||||||
|
Username = username;
|
||||||
|
Password = password;
|
||||||
|
}
|
||||||
|
}
|
||||||
38
src/Kyoo.Authentication/Models/DTO/PasswordResetRequest.cs
Normal file
38
src/Kyoo.Authentication/Models/DTO/PasswordResetRequest.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models.DTO;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A model only used on password resets.
|
||||||
|
/// </summary>
|
||||||
|
public class PasswordResetRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The old password
|
||||||
|
/// </summary>
|
||||||
|
public string? OldPassword { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The new password
|
||||||
|
/// </summary>
|
||||||
|
[MinLength(4, ErrorMessage = "The password must have at least {1} characters")]
|
||||||
|
public string NewPassword { get; set; }
|
||||||
|
}
|
||||||
76
src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs
Normal file
76
src/Kyoo.Authentication/Models/DTO/RegisterRequest.cs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
using BCryptNet = BCrypt.Net.BCrypt;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models.DTO;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A model only used on register requests.
|
||||||
|
/// </summary>
|
||||||
|
public class RegisterRequest
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The user email address
|
||||||
|
/// </summary>
|
||||||
|
[EmailAddress(ErrorMessage = "The email must be a valid email address")]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's username.
|
||||||
|
/// </summary>
|
||||||
|
[MinLength(4, ErrorMessage = "The username must have at least {1} characters")]
|
||||||
|
public string Username { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The user's password.
|
||||||
|
/// </summary>
|
||||||
|
[MinLength(4, ErrorMessage = "The password must have at least {1} characters")]
|
||||||
|
public string Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="RegisterRequest"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="email">The user email address.</param>
|
||||||
|
/// <param name="username">The user's username.</param>
|
||||||
|
/// <param name="password">The user's password.</param>
|
||||||
|
public RegisterRequest(string email, string username, string password)
|
||||||
|
{
|
||||||
|
Email = email;
|
||||||
|
Username = username;
|
||||||
|
Password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Convert this register request to a new <see cref="User"/> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A user representing this request.</returns>
|
||||||
|
public User ToUser()
|
||||||
|
{
|
||||||
|
return new User
|
||||||
|
{
|
||||||
|
Slug = Utility.ToSlug(Username),
|
||||||
|
Username = Username,
|
||||||
|
Password = BCryptNet.HashPassword(Password),
|
||||||
|
Email = Email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
98
src/Kyoo.Authentication/Models/DTO/ServerInfo.cs
Normal file
98
src/Kyoo.Authentication/Models/DTO/ServerInfo.cs
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models;
|
||||||
|
|
||||||
|
public class ServerInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The list of oidc providers configured for this instance of kyoo.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, OidcInfo> Oidc { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The url to reach the homepage of kyoo (add /api for the api).
|
||||||
|
/// </summary>
|
||||||
|
public string PublicUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if guest accounts are allowed on this instance.
|
||||||
|
/// </summary>
|
||||||
|
public bool AllowGuests { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if new users needs to be verifed.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequireVerification { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of permissions available for the guest account.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> GuestPermissions { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if kyoo's setup is finished.
|
||||||
|
/// </summary>
|
||||||
|
public SetupStep SetupStatus { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if password login is enabled on this instance.
|
||||||
|
/// </summary>
|
||||||
|
public bool PasswordLoginEnabled { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if registration is enabled on this instance.
|
||||||
|
/// </summary>
|
||||||
|
public bool RegistrationEnabled { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OidcInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of this oidc service. Human readable.
|
||||||
|
/// </summary>
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A url returing a square logo for this provider.
|
||||||
|
/// </summary>
|
||||||
|
public string? LogoUrl { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if kyoo's setup is finished.
|
||||||
|
/// </summary>
|
||||||
|
public enum SetupStep
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No admin account exists, create an account before exposing kyoo to the internet!
|
||||||
|
/// </summary>
|
||||||
|
MissingAdminAccount,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// No video was registered on kyoo, have you configured the rigth library path?
|
||||||
|
/// </summary>
|
||||||
|
NoVideoFound,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Setup finished!
|
||||||
|
/// </summary>
|
||||||
|
Done,
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models;
|
||||||
|
|
||||||
|
public class AuthenticationOption
|
||||||
|
{
|
||||||
|
public byte[] Secret { get; set; }
|
||||||
|
}
|
||||||
180
src/Kyoo.Authentication/Models/Options/PermissionOption.cs
Normal file
180
src/Kyoo.Authentication/Models/Options/PermissionOption.cs
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Kyoo.Abstractions.Models.Permissions;
|
||||||
|
using Kyoo.Authentication.Models.DTO;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Permission options.
|
||||||
|
/// </summary>
|
||||||
|
public class PermissionOption
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The path to get this option from the root configuration.
|
||||||
|
/// </summary>
|
||||||
|
public const string Path = "authentication:permissions";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// True if new users needs to be verifed.
|
||||||
|
/// </summary>
|
||||||
|
public bool RequireVerification { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default permissions that will be given to a non-connected user.
|
||||||
|
/// </summary>
|
||||||
|
public string[] Default { get; set; } = { "overall.read", "overall.play" };
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Permissions applied to a new user.
|
||||||
|
/// </summary>
|
||||||
|
public string[] NewUser { get; set; } = { "overall.read", "overall.play" };
|
||||||
|
|
||||||
|
public static string[] Admin =>
|
||||||
|
Enum.GetNames<Group>()
|
||||||
|
.Where(x => x != nameof(Group.None))
|
||||||
|
.SelectMany(group =>
|
||||||
|
Enum.GetNames<Kind>().Select(kind => $"{group}.{kind}".ToLowerInvariant())
|
||||||
|
)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The list of available ApiKeys.
|
||||||
|
/// </summary>
|
||||||
|
public string[] ApiKeys { get; set; } = Array.Empty<string>();
|
||||||
|
|
||||||
|
public string PublicUrl { get; set; }
|
||||||
|
|
||||||
|
public Dictionary<string, OidcProvider> OIDC { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum AuthMethod
|
||||||
|
{
|
||||||
|
ClientSecretBasic,
|
||||||
|
ClientSecretPost,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OidcProvider
|
||||||
|
{
|
||||||
|
public string DisplayName { get; set; }
|
||||||
|
public string? LogoUrl { get; set; }
|
||||||
|
public string AuthorizationUrl { get; set; }
|
||||||
|
public string TokenUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Some token endpoints do net respect the spec and require a json body instead of a form url encoded.
|
||||||
|
/// </summary>
|
||||||
|
public bool TokenUseJsonBody { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The OIDC spec allows multiples ways of authorizing the client.
|
||||||
|
/// </summary>
|
||||||
|
public AuthMethod ClientAuthMethod { get; set; } = AuthMethod.ClientSecretBasic;
|
||||||
|
|
||||||
|
public string ProfileUrl { get; set; }
|
||||||
|
public string? Scope { get; set; }
|
||||||
|
public string ClientId { get; set; }
|
||||||
|
public string Secret { get; set; }
|
||||||
|
|
||||||
|
public Func<JwtProfile, string?>? GetProfileUrl { get; init; }
|
||||||
|
public Func<OidcProvider, Dictionary<string, string>>? GetExtraHeaders { get; init; }
|
||||||
|
|
||||||
|
public bool Enabled =>
|
||||||
|
AuthorizationUrl != null
|
||||||
|
&& TokenUrl != null
|
||||||
|
&& ProfileUrl != null
|
||||||
|
&& ClientId != null
|
||||||
|
&& Secret != null;
|
||||||
|
|
||||||
|
public OidcProvider(string provider)
|
||||||
|
{
|
||||||
|
DisplayName = provider;
|
||||||
|
if (KnownProviders?.ContainsKey(provider) == true)
|
||||||
|
{
|
||||||
|
DisplayName = KnownProviders[provider].DisplayName;
|
||||||
|
LogoUrl = KnownProviders[provider].LogoUrl;
|
||||||
|
AuthorizationUrl = KnownProviders[provider].AuthorizationUrl;
|
||||||
|
TokenUrl = KnownProviders[provider].TokenUrl;
|
||||||
|
ProfileUrl = KnownProviders[provider].ProfileUrl;
|
||||||
|
Scope = KnownProviders[provider].Scope;
|
||||||
|
ClientId = KnownProviders[provider].ClientId;
|
||||||
|
Secret = KnownProviders[provider].Secret;
|
||||||
|
TokenUseJsonBody = KnownProviders[provider].TokenUseJsonBody;
|
||||||
|
ClientAuthMethod = KnownProviders[provider].ClientAuthMethod;
|
||||||
|
GetProfileUrl = KnownProviders[provider].GetProfileUrl;
|
||||||
|
GetExtraHeaders = KnownProviders[provider].GetExtraHeaders;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly Dictionary<string, OidcProvider> KnownProviders =
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
["google"] = new("google")
|
||||||
|
{
|
||||||
|
DisplayName = "Google",
|
||||||
|
LogoUrl = "https://logo.clearbit.com/google.com",
|
||||||
|
AuthorizationUrl = "https://accounts.google.com/o/oauth2/v2/auth",
|
||||||
|
TokenUrl = "https://oauth2.googleapis.com/token",
|
||||||
|
ProfileUrl = "https://openidconnect.googleapis.com/v1/userinfo",
|
||||||
|
Scope = "email profile",
|
||||||
|
},
|
||||||
|
["discord"] = new("discord")
|
||||||
|
{
|
||||||
|
DisplayName = "Discord",
|
||||||
|
LogoUrl = "https://logo.clearbit.com/discord.com",
|
||||||
|
AuthorizationUrl = "https://discord.com/oauth2/authorize",
|
||||||
|
TokenUrl = "https://discord.com/api/oauth2/token",
|
||||||
|
ProfileUrl = "https://discord.com/api/users/@me",
|
||||||
|
Scope = "email+identify",
|
||||||
|
},
|
||||||
|
["simkl"] = new("simkl")
|
||||||
|
{
|
||||||
|
DisplayName = "Simkl",
|
||||||
|
LogoUrl = "https://logo.clearbit.com/simkl.com",
|
||||||
|
AuthorizationUrl = "https://simkl.com/oauth/authorize",
|
||||||
|
TokenUrl = "https://api.simkl.com/oauth/token",
|
||||||
|
ProfileUrl = "https://api.simkl.com/users/settings",
|
||||||
|
// does not seems to have scopes
|
||||||
|
Scope = null,
|
||||||
|
TokenUseJsonBody = true,
|
||||||
|
ClientAuthMethod = AuthMethod.ClientSecretPost,
|
||||||
|
GetProfileUrl = (profile) => $"https://simkl.com/{profile.Sub}/dashboard/",
|
||||||
|
GetExtraHeaders = (OidcProvider self) =>
|
||||||
|
new() { ["simkl-api-key"] = self.ClientId },
|
||||||
|
},
|
||||||
|
["trakt"] = new("trakt")
|
||||||
|
{
|
||||||
|
DisplayName = "Trakt",
|
||||||
|
LogoUrl = "https://logo.clearbit.com/trakt.tv",
|
||||||
|
AuthorizationUrl = "https://api.trakt.tv/oauth/authorize",
|
||||||
|
TokenUrl = "https://api.trakt.tv/oauth/token",
|
||||||
|
ProfileUrl = "https://api.trakt.tv/users/settings",
|
||||||
|
// does not seems to have scopes
|
||||||
|
Scope = null,
|
||||||
|
TokenUseJsonBody = true,
|
||||||
|
GetProfileUrl = (profile) => $"https://trakt.tv/users/{profile.Username}",
|
||||||
|
GetExtraHeaders = (OidcProvider self) =>
|
||||||
|
new() { ["trakt-api-key"] = self.ClientId, ["trakt-api-version"] = "2", },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
501
src/Kyoo.Authentication/Views/AuthApi.cs
Normal file
501
src/Kyoo.Authentication/Views/AuthApi.cs
Normal file
@@ -0,0 +1,501 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
|
using Kyoo.Abstractions.Models.Permissions;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Kyoo.Authentication.Attributes;
|
||||||
|
using Kyoo.Authentication.Models;
|
||||||
|
using Kyoo.Authentication.Models.DTO;
|
||||||
|
using Kyoo.Models;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.IdentityModel.Tokens;
|
||||||
|
using static Kyoo.Abstractions.Models.Utils.Constants;
|
||||||
|
using BCryptNet = BCrypt.Net.BCrypt;
|
||||||
|
|
||||||
|
namespace Kyoo.Authentication.Views;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sign in, Sign up or refresh tokens.
|
||||||
|
/// </summary>
|
||||||
|
[ApiController]
|
||||||
|
[Route("auth")]
|
||||||
|
[ApiDefinition("Authentication", Group = UsersGroup)]
|
||||||
|
public class AuthApi(
|
||||||
|
IUserRepository users,
|
||||||
|
OidcController oidc,
|
||||||
|
ITokenController tokenController,
|
||||||
|
IThumbnailsManager thumbs,
|
||||||
|
PermissionOption options
|
||||||
|
) : ControllerBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a new Forbidden result from an object.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">The json value to output on the response.</param>
|
||||||
|
/// <returns>A new forbidden result with the given json object.</returns>
|
||||||
|
public static ObjectResult Forbid(object value)
|
||||||
|
{
|
||||||
|
return new ObjectResult(value) { StatusCode = StatusCodes.Status403Forbidden };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string _BuildUrl(string baseUrl, Dictionary<string, string?> queryParams)
|
||||||
|
{
|
||||||
|
char querySep = baseUrl.Contains('?') ? '&' : '?';
|
||||||
|
foreach ((string key, string? val) in queryParams)
|
||||||
|
{
|
||||||
|
if (val is null)
|
||||||
|
continue;
|
||||||
|
baseUrl += $"{querySep}{key}={val}";
|
||||||
|
querySep = '&';
|
||||||
|
}
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Oauth Login.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Login via a registered oauth provider.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="provider">The provider code.</param>
|
||||||
|
/// <param name="redirectUrl">
|
||||||
|
/// A url where you will be redirected with the query params provider, code and error. It can be a deep link.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>A redirect to the provider's login page.</returns>
|
||||||
|
/// <response code="404">The provider is not register with this instance of kyoo.</response>
|
||||||
|
[HttpGet("login/{provider}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(RequestError))]
|
||||||
|
public ActionResult<JwtToken> LoginVia(string provider, [FromQuery] string redirectUrl)
|
||||||
|
{
|
||||||
|
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
||||||
|
{
|
||||||
|
return NotFound(
|
||||||
|
new RequestError(
|
||||||
|
$"Invalid provider. {provider} is not registered no this instance of kyoo."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
OidcProvider prov = options.OIDC[provider];
|
||||||
|
return Redirect(
|
||||||
|
_BuildUrl(
|
||||||
|
prov.AuthorizationUrl,
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
["response_type"] = "code",
|
||||||
|
["client_id"] = prov.ClientId,
|
||||||
|
["redirect_uri"] =
|
||||||
|
$"{options.PublicUrl.TrimEnd('/')}/api/auth/logged/{provider}",
|
||||||
|
["scope"] = prov.Scope,
|
||||||
|
["state"] = redirectUrl,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Oauth Code Redirect.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This route is not meant to be called manually, the user should be redirected automatically here
|
||||||
|
/// after a successful login on the /login/{provider} page.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>A redirect to the provider's login page.</returns>
|
||||||
|
/// <response code="403">The provider gave an error.</response>
|
||||||
|
[HttpGet("logged/{provider}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status302Found)]
|
||||||
|
public ActionResult OauthCodeRedirect(string provider, string code, string state, string? error)
|
||||||
|
{
|
||||||
|
return Redirect(
|
||||||
|
_BuildUrl(
|
||||||
|
state,
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
["provider"] = provider,
|
||||||
|
["code"] = code,
|
||||||
|
["error"] = error,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Oauth callback
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This route should be manually called by the page that got redirected to after a call to /login/{provider}.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>A jwt token</returns>
|
||||||
|
/// <response code="400">Bad provider or code</response>
|
||||||
|
[HttpPost("callback/{provider}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<JwtToken>> OauthCallback(string provider, string code)
|
||||||
|
{
|
||||||
|
if (!options.OIDC.ContainsKey(provider) || !options.OIDC[provider].Enabled)
|
||||||
|
{
|
||||||
|
return NotFound(
|
||||||
|
new RequestError(
|
||||||
|
$"Invalid provider. {provider} is not registered no this instance of kyoo."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (code == null)
|
||||||
|
return BadRequest(new RequestError("Invalid code."));
|
||||||
|
|
||||||
|
Guid? userId = User.GetId();
|
||||||
|
User user = userId.HasValue
|
||||||
|
? await oidc.LinkAccountOrLogin(userId.Value, provider, code)
|
||||||
|
: await oidc.LoginViaCode(provider, code);
|
||||||
|
return new JwtToken(
|
||||||
|
tokenController.CreateAccessToken(user, out TimeSpan expireIn),
|
||||||
|
await tokenController.CreateRefreshToken(user),
|
||||||
|
expireIn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unlink account
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Unlink your account from an external account.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="provider">The provider code.</param>
|
||||||
|
/// <returns>Your updated user account</returns>
|
||||||
|
[HttpDelete("login/{provider}")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[UserOnly]
|
||||||
|
public Task<User> UnlinkAccount(string provider)
|
||||||
|
{
|
||||||
|
Guid id = User.GetIdOrThrow();
|
||||||
|
return users.DeleteExternalToken(id, provider);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Login.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Login as a user and retrieve an access and a refresh token.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="request">The body of the request.</param>
|
||||||
|
/// <returns>A new access and a refresh token.</returns>
|
||||||
|
/// <response code="403">The user and password does not match.</response>
|
||||||
|
[HttpPost("login")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
[DisableOnEnvVar("AUTHENTICATION_DISABLE_PASSWORD_LOGIN")]
|
||||||
|
public async Task<ActionResult<JwtToken>> Login([FromBody] LoginRequest request)
|
||||||
|
{
|
||||||
|
User? user = await users.GetOrDefault(
|
||||||
|
new Filter<User>.Eq(nameof(Abstractions.Models.User.Username), request.Username)
|
||||||
|
);
|
||||||
|
if (user != null && user.Password == null)
|
||||||
|
return Forbid(
|
||||||
|
new RequestError(
|
||||||
|
"This account was registerd via oidc. Please login via oidc or add a password to your account in the settings first"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (user == null || !BCryptNet.Verify(request.Password, user.Password))
|
||||||
|
return Forbid(new RequestError("The user and password does not match."));
|
||||||
|
|
||||||
|
return new JwtToken(
|
||||||
|
tokenController.CreateAccessToken(user, out TimeSpan expireIn),
|
||||||
|
await tokenController.CreateRefreshToken(user),
|
||||||
|
expireIn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Register.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Register a new user and get a new access/refresh token for this new user.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="request">The body of the request.</param>
|
||||||
|
/// <returns>A new access and a refresh token.</returns>
|
||||||
|
/// <response code="400">The request is invalid.</response>
|
||||||
|
/// <response code="409">A user already exists with this username or email address.</response>
|
||||||
|
[HttpPost("register")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status400BadRequest, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status409Conflict, Type = typeof(RequestError))]
|
||||||
|
[DisableOnEnvVar("AUTHENTICATION_DISABLE_USER_REGISTRATION")]
|
||||||
|
public async Task<ActionResult<JwtToken>> Register([FromBody] RegisterRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
User user = await users.Create(request.ToUser());
|
||||||
|
return new JwtToken(
|
||||||
|
tokenController.CreateAccessToken(user, out TimeSpan expireIn),
|
||||||
|
await tokenController.CreateRefreshToken(user),
|
||||||
|
expireIn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (DuplicatedItemException)
|
||||||
|
{
|
||||||
|
return Conflict(new RequestError("A user already exists with this username."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refresh a token.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Refresh an access token using the given refresh token. A new access and refresh token are generated.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="token">A valid refresh token.</param>
|
||||||
|
/// <returns>A new access and refresh token.</returns>
|
||||||
|
/// <response code="403">The given refresh token is invalid.</response>
|
||||||
|
[HttpGet("refresh")]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<JwtToken>> Refresh([FromQuery] string token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Guid userId = tokenController.GetRefreshTokenUserID(token);
|
||||||
|
User user = await users.Get(userId);
|
||||||
|
return new JwtToken(
|
||||||
|
tokenController.CreateAccessToken(user, out TimeSpan expireIn),
|
||||||
|
await tokenController.CreateRefreshToken(user),
|
||||||
|
expireIn
|
||||||
|
);
|
||||||
|
}
|
||||||
|
catch (ItemNotFoundException)
|
||||||
|
{
|
||||||
|
return Forbid(new RequestError("Invalid refresh token."));
|
||||||
|
}
|
||||||
|
catch (SecurityTokenException ex)
|
||||||
|
{
|
||||||
|
return Forbid(new RequestError(ex.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reset your password
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Change your password.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="request">The old and new password</param>
|
||||||
|
/// <returns>Your account info.</returns>
|
||||||
|
/// <response code="403">The old password is invalid.</response>
|
||||||
|
[HttpPost("password-reset")]
|
||||||
|
[UserOnly]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<User>> ResetPassword([FromBody] PasswordResetRequest request)
|
||||||
|
{
|
||||||
|
User user = await users.Get(User.GetIdOrThrow());
|
||||||
|
if (user.HasPassword && !BCryptNet.Verify(request.OldPassword, user.Password))
|
||||||
|
return Forbid(new RequestError("The old password is invalid."));
|
||||||
|
return await users.Patch(
|
||||||
|
user.Id,
|
||||||
|
(user) =>
|
||||||
|
{
|
||||||
|
user.Password = BCryptNet.HashPassword(request.NewPassword);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get authenticated user.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Get information about the currently authenticated user. This can also be used to ensure that you are
|
||||||
|
/// logged in.
|
||||||
|
/// </remarks>
|
||||||
|
/// <returns>The currently authenticated user.</returns>
|
||||||
|
/// <response code="401">The user is not authenticated.</response>
|
||||||
|
/// <response code="403">The given access token is invalid.</response>
|
||||||
|
[HttpGet("me")]
|
||||||
|
[UserOnly]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<User>> GetMe()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await users.Get(User.GetIdOrThrow());
|
||||||
|
}
|
||||||
|
catch (ItemNotFoundException)
|
||||||
|
{
|
||||||
|
return Forbid(new RequestError("Invalid token"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Edit self
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Edit information about the currently authenticated user.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="user">The new data for the current user.</param>
|
||||||
|
/// <returns>The currently authenticated user after modifications.</returns>
|
||||||
|
/// <response code="401">The user is not authenticated.</response>
|
||||||
|
/// <response code="403">The given access token is invalid.</response>
|
||||||
|
[HttpPut("me")]
|
||||||
|
[UserOnly]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<User>> EditMe(User user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
user.Id = User.GetIdOrThrow();
|
||||||
|
return await users.Edit(user);
|
||||||
|
}
|
||||||
|
catch (ItemNotFoundException)
|
||||||
|
{
|
||||||
|
return Forbid(new RequestError("Invalid token"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Patch self
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Edit only provided informations about the currently authenticated user.
|
||||||
|
/// </remarks>
|
||||||
|
/// <param name="patch">The new data for the current user.</param>
|
||||||
|
/// <returns>The currently authenticated user after modifications.</returns>
|
||||||
|
/// <response code="401">The user is not authenticated.</response>
|
||||||
|
/// <response code="403">The given access token is invalid.</response>
|
||||||
|
[HttpPatch("me")]
|
||||||
|
[UserOnly]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<User>> PatchMe([FromBody] Patch<User> patch)
|
||||||
|
{
|
||||||
|
Guid userId = User.GetIdOrThrow();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (patch.Id.HasValue && patch.Id != userId)
|
||||||
|
throw new ArgumentException("Can't edit your user id.");
|
||||||
|
if (patch.ContainsKey(nameof(Abstractions.Models.User.Password)))
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Can't edit your password via a PATCH. Use /auth/password-reset"
|
||||||
|
);
|
||||||
|
return await users.Patch(userId, patch.Apply);
|
||||||
|
}
|
||||||
|
catch (ItemNotFoundException)
|
||||||
|
{
|
||||||
|
return Forbid(new RequestError("Invalid token"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete account
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Delete the current account.
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="401">The user is not authenticated.</response>
|
||||||
|
/// <response code="403">The given access token is invalid.</response>
|
||||||
|
[HttpDelete("me")]
|
||||||
|
[UserOnly]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult<User>> DeleteMe()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await users.Delete(User.GetIdOrThrow());
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
catch (ItemNotFoundException)
|
||||||
|
{
|
||||||
|
return Forbid(new RequestError("Invalid token"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get profile picture
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Get your profile picture
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="401">The user is not authenticated.</response>
|
||||||
|
/// <response code="403">The given access token is invalid.</response>
|
||||||
|
[HttpGet("me/logo")]
|
||||||
|
[UserOnly]
|
||||||
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult> GetProfilePicture()
|
||||||
|
{
|
||||||
|
Stream img = await thumbs.GetUserImage(User.GetIdOrThrow());
|
||||||
|
// Allow clients to cache the image for 6 month.
|
||||||
|
Response.Headers.CacheControl = $"public, max-age={60 * 60 * 24 * 31 * 6}";
|
||||||
|
return File(img, "image/webp", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Set profile picture
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Set your profile picture
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="401">The user is not authenticated.</response>
|
||||||
|
/// <response code="403">The given access token is invalid.</response>
|
||||||
|
[HttpPost("me/logo")]
|
||||||
|
[UserOnly]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult> SetProfilePicture(IFormFile picture)
|
||||||
|
{
|
||||||
|
if (picture == null || picture.Length == 0)
|
||||||
|
return BadRequest();
|
||||||
|
await thumbs.SetUserImage(User.GetIdOrThrow(), picture.OpenReadStream());
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Delete profile picture
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// Delete your profile picture
|
||||||
|
/// </remarks>
|
||||||
|
/// <response code="401">The user is not authenticated.</response>
|
||||||
|
/// <response code="403">The given access token is invalid.</response>
|
||||||
|
[HttpDelete("me/logo")]
|
||||||
|
[UserOnly]
|
||||||
|
[ProducesResponseType(StatusCodes.Status204NoContent)]
|
||||||
|
[ProducesResponseType(StatusCodes.Status401Unauthorized, Type = typeof(RequestError))]
|
||||||
|
[ProducesResponseType(StatusCodes.Status403Forbidden, Type = typeof(RequestError))]
|
||||||
|
public async Task<ActionResult> DeleteProfilePicture()
|
||||||
|
{
|
||||||
|
await thumbs.SetUserImage(User.GetIdOrThrow(), null);
|
||||||
|
return NoContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
234
src/Kyoo.Core/.gitignore
vendored
Normal file
234
src/Kyoo.Core/.gitignore
vendored
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
## PROJECT CUSTOM IGNORES
|
||||||
|
libtranscoder.so
|
||||||
|
wwwroot/
|
||||||
|
|
||||||
|
## Ignore Visual Studio temporary files, build results, and
|
||||||
|
## files generated by popular Visual Studio add-ons.
|
||||||
|
|
||||||
|
# User-specific files
|
||||||
|
*.suo
|
||||||
|
*.user
|
||||||
|
*.userosscache
|
||||||
|
*.sln.docstates
|
||||||
|
|
||||||
|
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||||
|
*.userprefs
|
||||||
|
|
||||||
|
# Build results
|
||||||
|
[Dd]ebug/
|
||||||
|
[Dd]ebugPublic/
|
||||||
|
[Rr]elease/
|
||||||
|
[Rr]eleases/
|
||||||
|
x64/
|
||||||
|
x86/
|
||||||
|
build/
|
||||||
|
bld/
|
||||||
|
bin/
|
||||||
|
Bin/
|
||||||
|
obj/
|
||||||
|
Obj/
|
||||||
|
|
||||||
|
# Visual Studio 2015 cache/options directory
|
||||||
|
.vs/
|
||||||
|
|
||||||
|
# MSTest test Results
|
||||||
|
[Tt]est[Rr]esult*/
|
||||||
|
[Bb]uild[Ll]og.*
|
||||||
|
|
||||||
|
# NUNIT
|
||||||
|
*.VisualState.xml
|
||||||
|
TestResult.xml
|
||||||
|
|
||||||
|
# Build Results of an ATL Project
|
||||||
|
[Dd]ebugPS/
|
||||||
|
[Rr]eleasePS/
|
||||||
|
dlldata.c
|
||||||
|
|
||||||
|
*_i.c
|
||||||
|
*_p.c
|
||||||
|
*_i.h
|
||||||
|
*.ilk
|
||||||
|
*.meta
|
||||||
|
*.obj
|
||||||
|
*.pch
|
||||||
|
*.pdb
|
||||||
|
*.pgc
|
||||||
|
*.pgd
|
||||||
|
*.rsp
|
||||||
|
*.sbr
|
||||||
|
*.tlb
|
||||||
|
*.tli
|
||||||
|
*.tlh
|
||||||
|
*.tmp
|
||||||
|
*.tmp_proj
|
||||||
|
*.log
|
||||||
|
*.vspscc
|
||||||
|
*.vssscc
|
||||||
|
.builds
|
||||||
|
*.pidb
|
||||||
|
*.svclog
|
||||||
|
*.scc
|
||||||
|
|
||||||
|
# Chutzpah Test files
|
||||||
|
_Chutzpah*
|
||||||
|
|
||||||
|
# Visual C++ cache files
|
||||||
|
ipch/
|
||||||
|
*.aps
|
||||||
|
*.ncb
|
||||||
|
*.opendb
|
||||||
|
*.opensdf
|
||||||
|
*.sdf
|
||||||
|
*.cachefile
|
||||||
|
|
||||||
|
# Visual Studio profiler
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.vspx
|
||||||
|
*.sap
|
||||||
|
|
||||||
|
# TFS 2012 Local Workspace
|
||||||
|
$tf/
|
||||||
|
|
||||||
|
# Guidance Automation Toolkit
|
||||||
|
*.gpState
|
||||||
|
|
||||||
|
# ReSharper is a .NET coding add-in
|
||||||
|
_ReSharper*/
|
||||||
|
*.[Rr]e[Ss]harper
|
||||||
|
*.DotSettings.user
|
||||||
|
|
||||||
|
# JustCode is a .NET coding add-in
|
||||||
|
.JustCode
|
||||||
|
|
||||||
|
# TeamCity is a build add-in
|
||||||
|
_TeamCity*
|
||||||
|
|
||||||
|
# DotCover is a Code Coverage Tool
|
||||||
|
*.dotCover
|
||||||
|
|
||||||
|
# NCrunch
|
||||||
|
_NCrunch_*
|
||||||
|
.*crunch*.local.xml
|
||||||
|
nCrunchTemp_*
|
||||||
|
|
||||||
|
# MightyMoose
|
||||||
|
*.mm.*
|
||||||
|
AutoTest.Net/
|
||||||
|
|
||||||
|
# Web workbench (sass)
|
||||||
|
.sass-cache/
|
||||||
|
|
||||||
|
# Installshield output folder
|
||||||
|
[Ee]xpress/
|
||||||
|
|
||||||
|
# DocProject is a documentation generator add-in
|
||||||
|
DocProject/buildhelp/
|
||||||
|
DocProject/Help/*.HxT
|
||||||
|
DocProject/Help/*.HxC
|
||||||
|
DocProject/Help/*.hhc
|
||||||
|
DocProject/Help/*.hhk
|
||||||
|
DocProject/Help/*.hhp
|
||||||
|
DocProject/Help/Html2
|
||||||
|
DocProject/Help/html
|
||||||
|
|
||||||
|
# Click-Once directory
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Publish Web Output
|
||||||
|
*.[Pp]ublish.xml
|
||||||
|
*.azurePubxml
|
||||||
|
# but database connection strings (with potential passwords) will be unencrypted
|
||||||
|
*.pubxml
|
||||||
|
*.publishproj
|
||||||
|
|
||||||
|
# NuGet Packages
|
||||||
|
*.nupkg
|
||||||
|
# The packages folder can be ignored because of Package Restore
|
||||||
|
**/packages/*
|
||||||
|
# except build/, which is used as an MSBuild target.
|
||||||
|
!**/packages/build/
|
||||||
|
# Uncomment if necessary however generally it will be regenerated when needed
|
||||||
|
#!**/packages/repositories.config
|
||||||
|
|
||||||
|
# Microsoft Azure Build Output
|
||||||
|
csx/
|
||||||
|
*.build.csdef
|
||||||
|
|
||||||
|
# Microsoft Azure Emulator
|
||||||
|
ecf/
|
||||||
|
rcf/
|
||||||
|
|
||||||
|
# Microsoft Azure ApplicationInsights config file
|
||||||
|
ApplicationInsights.config
|
||||||
|
|
||||||
|
# Windows Store app package directory
|
||||||
|
AppPackages/
|
||||||
|
BundleArtifacts/
|
||||||
|
|
||||||
|
# Visual Studio cache files
|
||||||
|
# files ending in .cache can be ignored
|
||||||
|
*.[Cc]ache
|
||||||
|
# but keep track of directories ending in .cache
|
||||||
|
!*.[Cc]ache/
|
||||||
|
|
||||||
|
# Others
|
||||||
|
ClientBin/
|
||||||
|
~$*
|
||||||
|
*~
|
||||||
|
*.dbmdl
|
||||||
|
*.dbproj.schemaview
|
||||||
|
*.pfx
|
||||||
|
*.publishsettings
|
||||||
|
orleans.codegen.cs
|
||||||
|
|
||||||
|
/node_modules
|
||||||
|
|
||||||
|
# RIA/Silverlight projects
|
||||||
|
Generated_Code/
|
||||||
|
|
||||||
|
# Backup & report files from converting an old project file
|
||||||
|
# to a newer Visual Studio version. Backup files are not needed,
|
||||||
|
# because we have git ;-)
|
||||||
|
_UpgradeReport_Files/
|
||||||
|
Backup*/
|
||||||
|
UpgradeLog*.XML
|
||||||
|
UpgradeLog*.htm
|
||||||
|
|
||||||
|
# SQL Server files
|
||||||
|
*.mdf
|
||||||
|
*.ldf
|
||||||
|
|
||||||
|
# Business Intelligence projects
|
||||||
|
*.rdl.data
|
||||||
|
*.bim.layout
|
||||||
|
*.bim_*.settings
|
||||||
|
|
||||||
|
# Microsoft Fakes
|
||||||
|
FakesAssemblies/
|
||||||
|
|
||||||
|
# GhostDoc plugin setting file
|
||||||
|
*.GhostDoc.xml
|
||||||
|
|
||||||
|
# Node.js Tools for Visual Studio
|
||||||
|
.ntvs_analysis.dat
|
||||||
|
|
||||||
|
# Visual Studio 6 build log
|
||||||
|
*.plg
|
||||||
|
|
||||||
|
# Visual Studio 6 workspace options file
|
||||||
|
*.opt
|
||||||
|
|
||||||
|
# Visual Studio LightSwitch build output
|
||||||
|
**/*.HTMLClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/GeneratedArtifacts
|
||||||
|
**/*.DesktopClient/ModelManifest.xml
|
||||||
|
**/*.Server/GeneratedArtifacts
|
||||||
|
**/*.Server/ModelManifest.xml
|
||||||
|
_Pvt_Extensions
|
||||||
|
|
||||||
|
# Paket dependency manager
|
||||||
|
.paket/paket.exe
|
||||||
|
|
||||||
|
# FAKE - F# Make
|
||||||
|
.fake/
|
||||||
42
src/Kyoo.Core/Controllers/Base64RouteConstraint.cs
Normal file
42
src/Kyoo.Core/Controllers/Base64RouteConstraint.cs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
public class Base64RouteConstraint : IRouteConstraint
|
||||||
|
{
|
||||||
|
static Regex Base64Reg = new("^[-A-Za-z0-9+/]*={0,3}$");
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Match(
|
||||||
|
HttpContext? httpContext,
|
||||||
|
IRouter? route,
|
||||||
|
string routeKey,
|
||||||
|
RouteValueDictionary values,
|
||||||
|
RouteDirection routeDirection
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return values.TryGetValue(routeKey, out object? val)
|
||||||
|
&& val is string str
|
||||||
|
&& Base64Reg.IsMatch(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs
Normal file
41
src/Kyoo.Core/Controllers/IdentifierRouteConstraint.cs
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The route constraint that goes with the <see cref="Identifier"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class IdentifierRouteConstraint : IRouteConstraint
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool Match(
|
||||||
|
HttpContext? httpContext,
|
||||||
|
IRouter? route,
|
||||||
|
string routeKey,
|
||||||
|
RouteValueDictionary values,
|
||||||
|
RouteDirection routeDirection
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return values.ContainsKey(routeKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
src/Kyoo.Core/Controllers/LibraryManager.cs
Normal file
105
src/Kyoo.Core/Controllers/LibraryManager.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System.Linq;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An class to interact with the database. Every repository is mapped through here.
|
||||||
|
/// </summary>
|
||||||
|
public class LibraryManager : ILibraryManager
|
||||||
|
{
|
||||||
|
private readonly IBaseRepository[] _repositories;
|
||||||
|
|
||||||
|
public LibraryManager(
|
||||||
|
IRepository<ILibraryItem> libraryItemRepository,
|
||||||
|
IRepository<INews> newsRepository,
|
||||||
|
IWatchStatusRepository watchStatusRepository,
|
||||||
|
IRepository<Collection> collectionRepository,
|
||||||
|
IRepository<Movie> movieRepository,
|
||||||
|
IRepository<Show> showRepository,
|
||||||
|
IRepository<Season> seasonRepository,
|
||||||
|
IRepository<Episode> episodeRepository,
|
||||||
|
IRepository<Studio> studioRepository,
|
||||||
|
IRepository<User> userRepository
|
||||||
|
)
|
||||||
|
{
|
||||||
|
LibraryItems = libraryItemRepository;
|
||||||
|
News = newsRepository;
|
||||||
|
WatchStatus = watchStatusRepository;
|
||||||
|
Collections = collectionRepository;
|
||||||
|
Movies = movieRepository;
|
||||||
|
Shows = showRepository;
|
||||||
|
Seasons = seasonRepository;
|
||||||
|
Episodes = episodeRepository;
|
||||||
|
Studios = studioRepository;
|
||||||
|
Users = userRepository;
|
||||||
|
|
||||||
|
_repositories =
|
||||||
|
[
|
||||||
|
LibraryItems,
|
||||||
|
News,
|
||||||
|
Collections,
|
||||||
|
Movies,
|
||||||
|
Shows,
|
||||||
|
Seasons,
|
||||||
|
Episodes,
|
||||||
|
Studios,
|
||||||
|
Users
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<ILibraryItem> LibraryItems { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<INews> News { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IWatchStatusRepository WatchStatus { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<Collection> Collections { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<Movie> Movies { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<Show> Shows { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<Season> Seasons { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<Episode> Episodes { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<Studio> Studios { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IRepository<User> Users { get; }
|
||||||
|
|
||||||
|
public IRepository<T> Repository<T>()
|
||||||
|
where T : IResource, IQuery
|
||||||
|
{
|
||||||
|
return (IRepository<T>)_repositories.First(x => x.RepositoryType == typeof(T));
|
||||||
|
}
|
||||||
|
}
|
||||||
154
src/Kyoo.Core/Controllers/MiscRepository.cs
Normal file
154
src/Kyoo.Core/Controllers/MiscRepository.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Authentication.Models;
|
||||||
|
using Kyoo.Postgresql;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using static System.Text.Json.JsonNamingPolicy;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
public class MiscRepository(
|
||||||
|
DatabaseContext context,
|
||||||
|
DbConnection database,
|
||||||
|
IThumbnailsManager thumbnails
|
||||||
|
)
|
||||||
|
{
|
||||||
|
public static async Task DownloadMissingImages(IServiceProvider services)
|
||||||
|
{
|
||||||
|
await using AsyncServiceScope scope = services.CreateAsyncScope();
|
||||||
|
await scope.ServiceProvider.GetRequiredService<MiscRepository>().DownloadMissingImages();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ICollection<Image>> _GetAllImages()
|
||||||
|
{
|
||||||
|
string GetSql(string type) =>
|
||||||
|
$"""
|
||||||
|
select poster from {type}
|
||||||
|
union all select thumbnail from {type}
|
||||||
|
union all select logo from {type}
|
||||||
|
""";
|
||||||
|
var queries = new string[]
|
||||||
|
{
|
||||||
|
"movies",
|
||||||
|
"collections",
|
||||||
|
"shows",
|
||||||
|
"seasons",
|
||||||
|
"episodes"
|
||||||
|
}.Select(x => GetSql(x));
|
||||||
|
string sql = string.Join(" union all ", queries);
|
||||||
|
IEnumerable<Image?> ret = await database.QueryAsync<Image?>(sql);
|
||||||
|
return ret.ToArray() as Image[];
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DownloadMissingImages()
|
||||||
|
{
|
||||||
|
ICollection<Image> images = await _GetAllImages();
|
||||||
|
var tasks = images
|
||||||
|
.ToAsyncEnumerable()
|
||||||
|
.WhereAwait(async x => !await thumbnails.IsImageSaved(x.Id, ImageQuality.Low))
|
||||||
|
.Select(x => thumbnails.DownloadImage(x, x.Id.ToString()))
|
||||||
|
.ToEnumerable();
|
||||||
|
|
||||||
|
// Chunk tasks to prevent http timouts
|
||||||
|
foreach (IEnumerable<Task> batch in tasks.Chunk(30))
|
||||||
|
await Task.WhenAll(batch);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<string>> GetRegisteredPaths()
|
||||||
|
{
|
||||||
|
return await context
|
||||||
|
.Episodes.Select(x => x.Path)
|
||||||
|
.Concat(context.Movies.Select(x => x.Path))
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<int> DeletePath(string path, bool recurse)
|
||||||
|
{
|
||||||
|
// Make sure to include a path separator to prevents deletions from things like:
|
||||||
|
// DeletePath("/video/abc", true) -> /video/abdc (should not be deleted)
|
||||||
|
string dirPath = path.EndsWith("/") ? path : $"{path}/";
|
||||||
|
|
||||||
|
int count = await context
|
||||||
|
.Episodes.Where(x => x.Path == path || (recurse && x.Path.StartsWith(dirPath)))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
count += await context
|
||||||
|
.Movies.Where(x => x.Path == path || (recurse && x.Path.StartsWith(dirPath)))
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
await context
|
||||||
|
.Issues.Where(x =>
|
||||||
|
x.Domain == "scanner"
|
||||||
|
&& (x.Cause == path || (recurse && x.Cause.StartsWith(dirPath)))
|
||||||
|
)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<RefreshableItem>> GetRefreshableItems(DateTime end)
|
||||||
|
{
|
||||||
|
IQueryable<RefreshableItem> GetItems<T>()
|
||||||
|
where T : class, IResource, IRefreshable
|
||||||
|
{
|
||||||
|
return context
|
||||||
|
.Set<T>()
|
||||||
|
.Select(x => new RefreshableItem
|
||||||
|
{
|
||||||
|
Kind = CamelCase.ConvertName(typeof(T).Name),
|
||||||
|
Id = x.Id,
|
||||||
|
RefreshDate = x.NextMetadataRefresh!.Value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return await GetItems<Show>()
|
||||||
|
.Concat(GetItems<Movie>())
|
||||||
|
.Concat(GetItems<Season>())
|
||||||
|
.Concat(GetItems<Episode>())
|
||||||
|
.Concat(GetItems<Collection>())
|
||||||
|
.Where(x => x.RefreshDate <= end)
|
||||||
|
.OrderBy(x => x.RefreshDate)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<SetupStep> GetSetupStep()
|
||||||
|
{
|
||||||
|
bool hasUser = await context.Users.AnyAsync();
|
||||||
|
if (!hasUser)
|
||||||
|
return SetupStep.MissingAdminAccount;
|
||||||
|
bool hasItem = await context.Movies.AnyAsync() || await context.Shows.AnyAsync();
|
||||||
|
return hasItem ? SetupStep.Done : SetupStep.NoVideoFound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RefreshableItem
|
||||||
|
{
|
||||||
|
public string Kind { get; set; }
|
||||||
|
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public DateTime RefreshDate { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Kyoo.Postgresql;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A local repository to handle collections
|
||||||
|
/// </summary>
|
||||||
|
public class CollectionRepository(DatabaseContext database, IThumbnailsManager thumbnails)
|
||||||
|
: GenericRepository<Collection>(database)
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override async Task<ICollection<Collection>> Search(
|
||||||
|
string query,
|
||||||
|
Include<Collection>? include = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return await AddIncludes(Database.Collections, include)
|
||||||
|
.Where(x => EF.Functions.ILike(x.Name + " " + x.Slug, $"%{query}%"))
|
||||||
|
.Take(20)
|
||||||
|
.ToListAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task Validate(Collection resource)
|
||||||
|
{
|
||||||
|
await base.Validate(resource);
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(resource.Name))
|
||||||
|
throw new ArgumentException("The collection's name must be set and not empty");
|
||||||
|
resource.NextMetadataRefresh ??= DateTime.UtcNow.AddMonths(2);
|
||||||
|
await thumbnails.DownloadImages(resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddMovie(Guid id, Guid movieId)
|
||||||
|
{
|
||||||
|
Database.AddLinks<Collection, Movie>(id, movieId);
|
||||||
|
await Database.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddShow(Guid id, Guid showId)
|
||||||
|
{
|
||||||
|
Database.AddLinks<Collection, Show>(id, showId);
|
||||||
|
await Database.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
415
src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
Normal file
415
src/Kyoo.Core/Controllers/Repositories/DapperHelper.cs
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel.DataAnnotations.Schema;
|
||||||
|
using System.Data;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Dapper;
|
||||||
|
using InterpolatedSql.Dapper;
|
||||||
|
using InterpolatedSql.Dapper.SqlBuilders;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Attributes;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
using Kyoo.Authentication;
|
||||||
|
using Kyoo.Utils;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
public static class DapperHelper
|
||||||
|
{
|
||||||
|
public static SqlBuilder ProcessVariables(SqlBuilder sql, SqlVariableContext context)
|
||||||
|
{
|
||||||
|
int start = 0;
|
||||||
|
while ((start = sql.IndexOf("[", start, false)) != -1)
|
||||||
|
{
|
||||||
|
int end = sql.IndexOf("]", start, false);
|
||||||
|
if (end == -1)
|
||||||
|
throw new ArgumentException("Invalid sql variable substitue (missing ])");
|
||||||
|
string var = sql.Format[(start + 1)..end];
|
||||||
|
sql.Remove(start, end - start + 1);
|
||||||
|
sql.Insert(start, $"{context.ReadVar(var)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Property(string key, Dictionary<string, Type> config)
|
||||||
|
{
|
||||||
|
if (key == "kind")
|
||||||
|
return "kind";
|
||||||
|
string[] keys = config
|
||||||
|
.Where(x => !x.Key.StartsWith('_'))
|
||||||
|
// If first char is lower, assume manual sql instead of reflection.
|
||||||
|
.Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null)
|
||||||
|
.Select(x =>
|
||||||
|
$"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}"
|
||||||
|
)
|
||||||
|
.ToArray();
|
||||||
|
if (keys.Length == 1)
|
||||||
|
return keys.First();
|
||||||
|
return $"coalesce({string.Join(", ", keys)})";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ProcessSort<T>(
|
||||||
|
Sort<T> sort,
|
||||||
|
bool reverse,
|
||||||
|
Dictionary<string, Type> config,
|
||||||
|
bool recurse = false
|
||||||
|
)
|
||||||
|
where T : IQuery
|
||||||
|
{
|
||||||
|
string ret = sort switch
|
||||||
|
{
|
||||||
|
Sort<T>.Default(var value) => ProcessSort(value, reverse, config, true),
|
||||||
|
Sort<T>.By(string key, bool desc)
|
||||||
|
=> $"{Property(key, config)} {(desc ^ reverse ? "desc" : "asc")}",
|
||||||
|
Sort<T>.Random(var seed)
|
||||||
|
=> $"md5('{seed}' || {Property("id", config)}) {(reverse ? "desc" : "asc")}",
|
||||||
|
Sort<T>.Conglomerate(var list)
|
||||||
|
=> string.Join(", ", list.Select(x => ProcessSort(x, reverse, config, true))),
|
||||||
|
_ => throw new SwitchExpressionException(),
|
||||||
|
};
|
||||||
|
if (recurse)
|
||||||
|
return ret;
|
||||||
|
// always end query by an id sort.
|
||||||
|
return $"{ret}, {Property("id", config)} {(reverse ? "desc" : "asc")}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static (
|
||||||
|
string projection,
|
||||||
|
string join,
|
||||||
|
List<Type> types,
|
||||||
|
Func<T, IEnumerable<object?>, T> map
|
||||||
|
) ProcessInclude<T>(Include<T> include, Dictionary<string, Type> config)
|
||||||
|
where T : class
|
||||||
|
{
|
||||||
|
int relation = 0;
|
||||||
|
List<Type> types = new();
|
||||||
|
StringBuilder projection = new();
|
||||||
|
StringBuilder join = new();
|
||||||
|
|
||||||
|
foreach (Include.Metadata metadata in include.Metadatas)
|
||||||
|
{
|
||||||
|
relation++;
|
||||||
|
switch (metadata)
|
||||||
|
{
|
||||||
|
case Include.SingleRelation(var name, var type, var rid):
|
||||||
|
string tableName =
|
||||||
|
type.GetCustomAttribute<TableAttribute>()?.Name
|
||||||
|
?? $"{type.Name.ToSnakeCase()}s";
|
||||||
|
types.Add(type);
|
||||||
|
projection.AppendLine($", r{relation}.* -- {type.Name} as r{relation}");
|
||||||
|
join.Append(
|
||||||
|
$"\nleft join {tableName} as r{relation} on r{relation}.id = {Property(rid, config)}"
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case Include.CustomRelation(var name, var type, var sql, var on, var declaring):
|
||||||
|
string owner = config.First(x => x.Value == declaring).Key;
|
||||||
|
string lateral = sql.Contains("\"this\"") ? " lateral" : string.Empty;
|
||||||
|
sql = sql.Replace("\"this\"", owner);
|
||||||
|
on = on?.Replace("\"this\"", owner)?.Replace("\"relation\"", $"r{relation}");
|
||||||
|
if (sql.Any(char.IsWhiteSpace))
|
||||||
|
sql = $"({sql})";
|
||||||
|
types.Add(type);
|
||||||
|
projection.AppendLine($", r{relation}.*");
|
||||||
|
join.Append($"\nleft join{lateral} {sql} as r{relation} on r{relation}.{on}");
|
||||||
|
break;
|
||||||
|
case Include.ProjectedRelation:
|
||||||
|
continue;
|
||||||
|
default:
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
T Map(T item, IEnumerable<object?> relations)
|
||||||
|
{
|
||||||
|
IEnumerable<string> metadatas = include
|
||||||
|
.Metadatas.Where(x => x is not Include.ProjectedRelation)
|
||||||
|
.Select(x => x.Name);
|
||||||
|
foreach ((string name, object? value) in metadatas.Zip(relations))
|
||||||
|
{
|
||||||
|
if (value == null)
|
||||||
|
continue;
|
||||||
|
PropertyInfo? prop = item.GetType().GetProperty(name);
|
||||||
|
if (prop != null)
|
||||||
|
prop.SetValue(item, value);
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (projection.ToString(), join.ToString(), types, Map);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static FormattableString ProcessFilter<T>(
|
||||||
|
Filter<T> filter,
|
||||||
|
Dictionary<string, Type> config
|
||||||
|
)
|
||||||
|
{
|
||||||
|
FormattableString Format(string key, FormattableString op)
|
||||||
|
{
|
||||||
|
if (key == "kind")
|
||||||
|
{
|
||||||
|
string cases = string.Join(
|
||||||
|
'\n',
|
||||||
|
config
|
||||||
|
.Skip(1)
|
||||||
|
.Select(x =>
|
||||||
|
$"when {x.Key}.id is not null then '{x.Value.Name.ToLowerInvariant()}'"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return $"""
|
||||||
|
case
|
||||||
|
{cases:raw}
|
||||||
|
else '{config.First().Value.Name.ToLowerInvariant():raw}'
|
||||||
|
end {op}
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerable<string> properties = config
|
||||||
|
.Where(x => !x.Key.StartsWith('_'))
|
||||||
|
// If first char is lower, assume manual sql instead of reflection.
|
||||||
|
.Where(x => char.IsLower(key.First()) || x.Value.GetProperty(key) != null)
|
||||||
|
.Select(x =>
|
||||||
|
$"{x.Key}.{x.Value.GetProperty(key)?.GetCustomAttribute<ColumnAttribute>()?.Name ?? key.ToSnakeCase()}"
|
||||||
|
);
|
||||||
|
|
||||||
|
FormattableString ret = $"{properties.First():raw} {op}";
|
||||||
|
foreach (string property in properties.Skip(1))
|
||||||
|
ret = $"{ret} or {property:raw} {op}";
|
||||||
|
return $"({ret})";
|
||||||
|
}
|
||||||
|
|
||||||
|
object P(object value)
|
||||||
|
{
|
||||||
|
if (value is Enum)
|
||||||
|
return new Wrapper(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
FormattableString Process(Filter<T> fil)
|
||||||
|
{
|
||||||
|
return fil switch
|
||||||
|
{
|
||||||
|
Filter<T>.And(var first, var second) => $"({Process(first)} and {Process(second)})",
|
||||||
|
Filter<T>.Or(var first, var second) => $"({Process(first)} or {Process(second)})",
|
||||||
|
Filter<T>.Not(var inner) => $"(not {Process(inner)})",
|
||||||
|
Filter<T>.Eq(var property, var value) when value is null
|
||||||
|
=> Format(property, $"is null"),
|
||||||
|
Filter<T>.Ne(var property, var value) when value is null
|
||||||
|
=> Format(property, $"is not null"),
|
||||||
|
Filter<T>.Eq(var property, var value) => Format(property, $"= {P(value!)}"),
|
||||||
|
Filter<T>.Ne(var property, var value) => Format(property, $"!= {P(value!)}"),
|
||||||
|
Filter<T>.Gt(var property, var value) => Format(property, $"> {P(value)}"),
|
||||||
|
Filter<T>.Ge(var property, var value) => Format(property, $">= {P(value)}"),
|
||||||
|
Filter<T>.Lt(var property, var value) => Format(property, $"< {P(value)}"),
|
||||||
|
Filter<T>.Le(var property, var value) => Format(property, $"> {P(value)}"),
|
||||||
|
Filter<T>.Has(var property, var value)
|
||||||
|
=> $"{P(value)} = any({Property(property, config):raw})",
|
||||||
|
Filter<T>.CmpRandom(var op, var seed, var id)
|
||||||
|
=> $"md5({seed} || coalesce({string.Join(", ", config.Select(x => $"{x.Key}.id")):raw})) {op:raw} md5({seed} || {id.ToString()})",
|
||||||
|
Filter<T>.Lambda(var lambda) => throw new NotSupportedException(),
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return Process(filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ExpendProjections(Type type, string? prefix, Include include)
|
||||||
|
{
|
||||||
|
IEnumerable<string> projections = include
|
||||||
|
.Metadatas.Select(x => (x as Include.ProjectedRelation)!)
|
||||||
|
.Where(x => x != null)
|
||||||
|
.Where(x => type.GetProperty(x.Name) != null)
|
||||||
|
.Select(x => x.Sql.Replace("\"this\".", prefix));
|
||||||
|
return string.Join(", ", projections);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<ICollection<T>> Query<T>(
|
||||||
|
this IDbConnection db,
|
||||||
|
FormattableString command,
|
||||||
|
Dictionary<string, Type> config,
|
||||||
|
Func<IList<object?>, T> mapper,
|
||||||
|
Func<Guid, Task<T>> get,
|
||||||
|
SqlVariableContext context,
|
||||||
|
Include<T>? include,
|
||||||
|
Filter<T>? filter,
|
||||||
|
Sort<T>? sort,
|
||||||
|
Pagination? limit
|
||||||
|
)
|
||||||
|
where T : class, IResource, IQuery
|
||||||
|
{
|
||||||
|
SqlBuilder query = new(db, command);
|
||||||
|
|
||||||
|
// Include handling
|
||||||
|
include ??= new();
|
||||||
|
var (includeProjection, includeJoin, includeTypes, mapIncludes) = ProcessInclude(
|
||||||
|
include,
|
||||||
|
config
|
||||||
|
);
|
||||||
|
query.Replace("/* includesJoin */", $"{includeJoin:raw}", out bool replaced);
|
||||||
|
if (!replaced)
|
||||||
|
query.AppendLiteral(includeJoin);
|
||||||
|
query.Replace("/* includes */", $"{includeProjection:raw}", out replaced);
|
||||||
|
if (!replaced)
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Missing '/* includes */' placeholder in top level sql select to support includes."
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle pagination, orders and filter.
|
||||||
|
if (limit?.AfterID != null)
|
||||||
|
{
|
||||||
|
T reference = await get(limit.AfterID.Value);
|
||||||
|
Filter<T>? keysetFilter = RepositoryHelper.KeysetPaginate(
|
||||||
|
sort,
|
||||||
|
reference,
|
||||||
|
!limit.Reverse
|
||||||
|
);
|
||||||
|
filter = Filter.And(filter, keysetFilter);
|
||||||
|
}
|
||||||
|
if (filter != null)
|
||||||
|
{
|
||||||
|
FormattableString filterSql = ProcessFilter(filter, config);
|
||||||
|
query.Replace("/* where */", $"and {filterSql}", out replaced);
|
||||||
|
if (!replaced)
|
||||||
|
query += $"\nwhere {filterSql}";
|
||||||
|
}
|
||||||
|
if (sort != null)
|
||||||
|
query += $"\norder by {ProcessSort(sort, limit?.Reverse ?? false, config):raw}";
|
||||||
|
if (limit != null)
|
||||||
|
query += $"\nlimit {limit.Limit}";
|
||||||
|
|
||||||
|
ProcessVariables(query, context);
|
||||||
|
|
||||||
|
// Build query and prepare to do the query/projections
|
||||||
|
IDapperSqlCommand cmd = query.Build();
|
||||||
|
string sql = cmd.Sql;
|
||||||
|
List<Type> types = config.Select(x => x.Value).Concat(includeTypes).ToList();
|
||||||
|
|
||||||
|
// Expand projections on every types received.
|
||||||
|
sql = Regex.Replace(
|
||||||
|
sql,
|
||||||
|
@"(,?) -- (\w+)( as (\w+))?",
|
||||||
|
(match) =>
|
||||||
|
{
|
||||||
|
string leadingComa = match.Groups[1].Value;
|
||||||
|
string type = match.Groups[2].Value;
|
||||||
|
string? prefix = match.Groups[4].Value;
|
||||||
|
prefix = !string.IsNullOrEmpty(prefix) ? $"{prefix}." : string.Empty;
|
||||||
|
|
||||||
|
Type typeV = types.First(x => x.Name == type);
|
||||||
|
|
||||||
|
// Only project top level items with explicit includes.
|
||||||
|
string? projection = config.Any(x => x.Value.Name == type)
|
||||||
|
? ExpendProjections(typeV, prefix, include)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(projection))
|
||||||
|
return leadingComa;
|
||||||
|
return $", {projection}{leadingComa}";
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
IEnumerable<T> data = await db.QueryAsync<T>(
|
||||||
|
sql,
|
||||||
|
types.ToArray(),
|
||||||
|
items =>
|
||||||
|
{
|
||||||
|
return mapIncludes(mapper(items), items.Skip(config.Count));
|
||||||
|
},
|
||||||
|
ParametersDictionary.LoadFrom(cmd),
|
||||||
|
splitOn: string.Join(
|
||||||
|
',',
|
||||||
|
types.Select(x => x.GetCustomAttribute<SqlFirstColumnAttribute>()?.Name ?? "id")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
if (limit?.Reverse == true)
|
||||||
|
data = data.Reverse();
|
||||||
|
return data.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<T?> QuerySingle<T>(
|
||||||
|
this IDbConnection db,
|
||||||
|
FormattableString command,
|
||||||
|
Dictionary<string, Type> config,
|
||||||
|
Func<IList<object?>, T> mapper,
|
||||||
|
SqlVariableContext context,
|
||||||
|
Include<T>? include,
|
||||||
|
Filter<T>? filter,
|
||||||
|
Sort<T>? sort = null,
|
||||||
|
bool reverse = false,
|
||||||
|
Guid? afterId = default
|
||||||
|
)
|
||||||
|
where T : class, IResource, IQuery
|
||||||
|
{
|
||||||
|
ICollection<T> ret = await db.Query<T>(
|
||||||
|
command,
|
||||||
|
config,
|
||||||
|
mapper,
|
||||||
|
get: null!,
|
||||||
|
context,
|
||||||
|
include,
|
||||||
|
filter,
|
||||||
|
sort,
|
||||||
|
new Pagination(1, afterId, reverse)
|
||||||
|
);
|
||||||
|
return ret.FirstOrDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<int> Count<T>(
|
||||||
|
this IDbConnection db,
|
||||||
|
FormattableString command,
|
||||||
|
Dictionary<string, Type> config,
|
||||||
|
SqlVariableContext context,
|
||||||
|
Filter<T>? filter
|
||||||
|
)
|
||||||
|
where T : class, IResource
|
||||||
|
{
|
||||||
|
SqlBuilder query = new(db, command);
|
||||||
|
|
||||||
|
if (filter != null)
|
||||||
|
query += ProcessFilter(filter, config);
|
||||||
|
ProcessVariables(query, context);
|
||||||
|
IDapperSqlCommand cmd = query.Build();
|
||||||
|
|
||||||
|
// language=postgreSQL
|
||||||
|
string sql = $"select count(*) from ({cmd.Sql}) as query";
|
||||||
|
|
||||||
|
return await db.QuerySingleAsync<int>(sql, ParametersDictionary.LoadFrom(cmd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SqlVariableContext(IHttpContextAccessor accessor)
|
||||||
|
{
|
||||||
|
public object? ReadVar(string var)
|
||||||
|
{
|
||||||
|
return var switch
|
||||||
|
{
|
||||||
|
"current_user" => accessor.HttpContext?.User.GetId(),
|
||||||
|
_ => throw new ArgumentException($"Invalid sql variable name: {var}")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
221
src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
Normal file
221
src/Kyoo.Core/Controllers/Repositories/DapperRepository.cs
Normal file
@@ -0,0 +1,221 @@
|
|||||||
|
// Kyoo - A portable and vast media library solution.
|
||||||
|
// Copyright (c) Kyoo.
|
||||||
|
//
|
||||||
|
// See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||||
|
//
|
||||||
|
// Kyoo is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// any later version.
|
||||||
|
//
|
||||||
|
// Kyoo is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License
|
||||||
|
// along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data.Common;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Kyoo.Abstractions.Controllers;
|
||||||
|
using Kyoo.Abstractions.Models;
|
||||||
|
using Kyoo.Abstractions.Models.Exceptions;
|
||||||
|
using Kyoo.Abstractions.Models.Utils;
|
||||||
|
|
||||||
|
namespace Kyoo.Core.Controllers;
|
||||||
|
|
||||||
|
public abstract class DapperRepository<T> : IRepository<T>
|
||||||
|
where T : class, IResource, IQuery
|
||||||
|
{
|
||||||
|
public Type RepositoryType => typeof(T);
|
||||||
|
|
||||||
|
protected abstract FormattableString Sql { get; }
|
||||||
|
|
||||||
|
protected abstract Dictionary<string, Type> Config { get; }
|
||||||
|
|
||||||
|
protected abstract T Mapper(IList<object?> items);
|
||||||
|
|
||||||
|
protected DbConnection Database { get; init; }
|
||||||
|
|
||||||
|
protected SqlVariableContext Context { get; init; }
|
||||||
|
|
||||||
|
public DapperRepository(DbConnection database, SqlVariableContext context)
|
||||||
|
{
|
||||||
|
Database = database;
|
||||||
|
Context = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public virtual async Task<T> Get(Guid id, Include<T>? include = default)
|
||||||
|
{
|
||||||
|
T? ret = await GetOrDefault(id, include);
|
||||||
|
if (ret == null)
|
||||||
|
throw new ItemNotFoundException($"No {typeof(T).Name} found with the id {id}");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public virtual async Task<T> Get(string slug, Include<T>? include = default)
|
||||||
|
{
|
||||||
|
T? ret = await GetOrDefault(slug, include);
|
||||||
|
if (ret == null)
|
||||||
|
throw new ItemNotFoundException($"No {typeof(T).Name} found with the slug {slug}");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc/>
|
||||||
|
public virtual async Task<T> Get(
|
||||||
|
Filter<T>? filter,
|
||||||
|
Include<T>? include = default,
|
||||||
|
Sort<T>? sortBy = default,
|
||||||
|
bool reverse = false,
|
||||||
|
Guid? afterId = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
T? ret = await GetOrDefault(filter, include, sortBy, reverse, afterId);
|
||||||
|
if (ret == null)
|
||||||
|
throw new ItemNotFoundException($"No {typeof(T).Name} found with the given predicate.");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ICollection<T>> FromIds(IList<Guid> ids, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
if (!ids.Any())
|
||||||
|
return Array.Empty<T>();
|
||||||
|
return (
|
||||||
|
await Database.Query<T>(
|
||||||
|
Sql,
|
||||||
|
Config,
|
||||||
|
Mapper,
|
||||||
|
(id) => Get(id),
|
||||||
|
Context,
|
||||||
|
include,
|
||||||
|
Filter.Or(ids.Select(x => new Filter<T>.Eq("id", x)).ToArray()),
|
||||||
|
sort: null,
|
||||||
|
limit: null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.OrderBy(x => ids.IndexOf(x.Id))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<T?> GetOrDefault(Guid id, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
return Database.QuerySingle<T>(
|
||||||
|
Sql,
|
||||||
|
Config,
|
||||||
|
Mapper,
|
||||||
|
Context,
|
||||||
|
include,
|
||||||
|
new Filter<T>.Eq(nameof(IResource.Id), id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<T?> GetOrDefault(string slug, Include<T>? include = null)
|
||||||
|
{
|
||||||
|
if (slug == "random")
|
||||||
|
{
|
||||||
|
return Database.QuerySingle<T>(
|
||||||
|
Sql,
|
||||||
|
Config,
|
||||||
|
Mapper,
|
||||||
|
Context,
|
||||||
|
include,
|
||||||
|
filter: null,
|
||||||
|
new Sort<T>.Random()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return Database.QuerySingle<T>(
|
||||||
|
Sql,
|
||||||
|
Config,
|
||||||
|
Mapper,
|
||||||
|
Context,
|
||||||
|
include,
|
||||||
|
new Filter<T>.Eq(nameof(IResource.Slug), slug)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public virtual Task<T?> GetOrDefault(
|
||||||
|
Filter<T>? filter,
|
||||||
|
Include<T>? include = default,
|
||||||
|
Sort<T>? sortBy = default,
|
||||||
|
bool reverse = false,
|
||||||
|
Guid? afterId = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return Database.QuerySingle<T>(
|
||||||
|
Sql,
|
||||||
|
Config,
|
||||||
|
Mapper,
|
||||||
|
Context,
|
||||||
|
include,
|
||||||
|
filter,
|
||||||
|
sortBy,
|
||||||
|
reverse,
|
||||||
|
afterId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<ICollection<T>> GetAll(
|
||||||
|
Filter<T>? filter = default,
|
||||||
|
Sort<T>? sort = default,
|
||||||
|
Include<T>? include = default,
|
||||||
|
Pagination? limit = default
|
||||||
|
)
|
||||||
|
{
|
||||||
|
return Database.Query<T>(
|
||||||
|
Sql,
|
||||||
|
Config,
|
||||||
|
Mapper,
|
||||||
|
(id) => Get(id),
|
||||||
|
Context,
|
||||||
|
include,
|
||||||
|
filter,
|
||||||
|
sort ?? new Sort<T>.Default(),
|
||||||
|
limit ?? new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<int> GetCount(Filter<T>? filter = null)
|
||||||
|
{
|
||||||
|
return Database.Count(Sql, Config, Context, filter);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<ICollection<T>> Search(string query, Include<T>? include = null) =>
|
||||||
|
throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<T> Create(T obj) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<T> CreateIfNotExists(T obj) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task Delete(Guid id) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task Delete(string slug) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task Delete(T obj) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task DeleteAll(Filter<T> filter) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<T> Edit(T edited) => throw new NotImplementedException();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public Task<T> Patch(Guid id, Func<T, T> patch) => throw new NotImplementedException();
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user