init: repo
This commit is contained in:
commit
a90302f7ba
210
Chiara/.editorconfig
Normal file
210
Chiara/.editorconfig
Normal file
|
@ -0,0 +1,210 @@
|
|||
# editorconfig.org
|
||||
|
||||
# top-most EditorConfig file
|
||||
root = true
|
||||
|
||||
# Default settings:
|
||||
# A newline ending every file
|
||||
# Use 4 spaces as indentation
|
||||
[*]
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[project.json]
|
||||
indent_size = 2
|
||||
|
||||
# C# and Visual Basic files
|
||||
[*.{cs,vb}]
|
||||
charset = utf-8-bom
|
||||
|
||||
# Analyzers
|
||||
dotnet_analyzer_diagnostic.category-Security.severity = error
|
||||
dotnet_code_quality.ca1802.api_surface = private, internal
|
||||
|
||||
# Miscellaneous style rules
|
||||
dotnet_sort_system_directives_first = true
|
||||
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
|
||||
dotnet_style_predefined_type_for_member_access = true:suggestion
|
||||
|
||||
# avoid this. unless absolutely 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
|
||||
|
||||
# name all constant fields using PascalCase
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields
|
||||
dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style
|
||||
dotnet_naming_symbols.constant_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.constant_fields.required_modifiers = const
|
||||
dotnet_naming_style.pascal_case_style.capitalization = pascal_case
|
||||
|
||||
# static fields should have s_ prefix
|
||||
dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion
|
||||
dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields
|
||||
dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style
|
||||
dotnet_naming_symbols.static_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.static_fields.required_modifiers = static
|
||||
dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected
|
||||
dotnet_naming_style.static_prefix_style.required_prefix = s_
|
||||
dotnet_naming_style.static_prefix_style.capitalization = camel_case
|
||||
|
||||
# internal and private fields should be _camelCase
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.severity = suggestion
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields
|
||||
dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style
|
||||
dotnet_naming_symbols.private_internal_fields.applicable_kinds = field
|
||||
dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal
|
||||
dotnet_naming_style.camel_case_underscore_style.required_prefix = _
|
||||
dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case
|
||||
|
||||
# Code quality
|
||||
dotnet_style_readonly_field = true:suggestion
|
||||
dotnet_code_quality_unused_parameters = non_public:suggestion
|
||||
|
||||
# Expression-level preferences
|
||||
dotnet_style_object_initializer = true:suggestion
|
||||
dotnet_style_collection_initializer = true:suggestion
|
||||
dotnet_style_explicit_tuple_names = true:suggestion
|
||||
dotnet_style_coalesce_expression = true:suggestion
|
||||
dotnet_style_null_propagation = true:suggestion
|
||||
dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
|
||||
dotnet_style_prefer_inferred_tuple_names = true:suggestion
|
||||
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
|
||||
dotnet_style_prefer_auto_properties = true:suggestion
|
||||
dotnet_style_prefer_conditional_expression_over_assignment = true:refactoring
|
||||
dotnet_style_prefer_conditional_expression_over_return = true:refactoring
|
||||
|
||||
# CA2208: Instantiate argument exceptions correctly
|
||||
dotnet_diagnostic.CA2208.severity = error
|
||||
|
||||
# C# files
|
||||
[*.cs]
|
||||
# New line preferences
|
||||
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 = true
|
||||
csharp_new_line_before_members_in_anonymous_types = true
|
||||
csharp_new_line_between_query_expression_clauses = true
|
||||
|
||||
# Indentation preferences
|
||||
csharp_indent_block_contents = true
|
||||
csharp_indent_braces = false
|
||||
csharp_indent_case_contents = true
|
||||
csharp_indent_case_contents_when_block = true
|
||||
csharp_indent_switch_labels = true
|
||||
csharp_indent_labels = one_less_than_current
|
||||
|
||||
# Modifier preferences
|
||||
csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion
|
||||
|
||||
# Code style defaults
|
||||
csharp_using_directive_placement = outside_namespace:suggestion
|
||||
csharp_prefer_braces = true:refactoring
|
||||
csharp_preserve_single_line_blocks = true:none
|
||||
csharp_preserve_single_line_statements = false:none
|
||||
csharp_prefer_static_local_function = true:suggestion
|
||||
csharp_prefer_simple_using_statement = false:none
|
||||
csharp_style_prefer_switch_expression = true:suggestion
|
||||
|
||||
# Expression-bodied members
|
||||
csharp_style_expression_bodied_methods = true:refactoring
|
||||
csharp_style_expression_bodied_constructors = true:refactoring
|
||||
csharp_style_expression_bodied_operators = true:refactoring
|
||||
csharp_style_expression_bodied_properties = true:refactoring
|
||||
csharp_style_expression_bodied_indexers = true:refactoring
|
||||
csharp_style_expression_bodied_accessors = true:refactoring
|
||||
csharp_style_expression_bodied_lambdas = true:refactoring
|
||||
csharp_style_expression_bodied_local_functions = true:refactoring
|
||||
|
||||
# Pattern matching
|
||||
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
|
||||
|
||||
# Expression-level preferences
|
||||
csharp_prefer_simple_default_expression = true:suggestion
|
||||
|
||||
# Null checking preferences
|
||||
csharp_style_throw_expression = true:suggestion
|
||||
csharp_style_conditional_delegate_call = true:suggestion
|
||||
|
||||
# Other features
|
||||
csharp_style_prefer_index_operator = false:none
|
||||
csharp_style_prefer_range_operator = false:none
|
||||
csharp_style_pattern_local_over_anonymous_function = false:none
|
||||
|
||||
# Space preferences
|
||||
csharp_space_after_cast = false
|
||||
csharp_space_after_colon_in_inheritance_clause = true
|
||||
csharp_space_after_comma = true
|
||||
csharp_space_after_dot = false
|
||||
csharp_space_after_keywords_in_control_flow_statements = true
|
||||
csharp_space_after_semicolon_in_for_statement = true
|
||||
csharp_space_around_binary_operators = before_and_after
|
||||
csharp_space_around_declaration_statements = do_not_ignore
|
||||
csharp_space_before_colon_in_inheritance_clause = true
|
||||
csharp_space_before_comma = false
|
||||
csharp_space_before_dot = false
|
||||
csharp_space_before_open_square_brackets = false
|
||||
csharp_space_before_semicolon_in_for_statement = false
|
||||
csharp_space_between_empty_square_brackets = false
|
||||
csharp_space_between_method_call_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_call_name_and_opening_parenthesis = false
|
||||
csharp_space_between_method_call_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
|
||||
csharp_space_between_method_declaration_name_and_open_parenthesis = false
|
||||
csharp_space_between_method_declaration_parameter_list_parentheses = false
|
||||
csharp_space_between_parentheses = false
|
||||
csharp_space_between_square_brackets = false
|
||||
|
||||
# Namespace preference
|
||||
csharp_style_namespace_declarations = file_scoped:suggestion
|
||||
|
||||
# Types: use keywords instead of BCL types, and permit var only when the type is clear
|
||||
csharp_style_var_for_built_in_types = false:suggestion
|
||||
csharp_style_var_when_type_is_apparent = false:none
|
||||
csharp_style_var_elsewhere = false:suggestion
|
||||
|
||||
# Visual Basic files
|
||||
[*.vb]
|
||||
# Modifier preferences
|
||||
visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion
|
||||
|
||||
# C++ Files
|
||||
[*.{cpp,h,in}]
|
||||
curly_bracket_next_line = true
|
||||
indent_brace_style = Allman
|
||||
|
||||
# Xml project files
|
||||
[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml build files
|
||||
[*.builds]
|
||||
indent_size = 2
|
||||
|
||||
# Xml files
|
||||
[*.{xml,stylecop,resx,ruleset}]
|
||||
indent_size = 2
|
||||
|
||||
# Xml config files
|
||||
[*.{props,targets,config,nuspec}]
|
||||
indent_size = 2
|
||||
|
||||
# Shell scripts
|
||||
[*.sh]
|
||||
end_of_line = lf
|
||||
|
||||
[*.{cmd, bat}]
|
||||
end_of_line = crlf
|
||||
|
||||
# Markdown files
|
||||
[*.md]
|
||||
# Double trailing spaces can be used for BR tags, and other instances are enforced by Markdownlint
|
||||
trim_trailing_whitespace = false
|
26
Chiara/.gitea/workflows/build.yaml
Normal file
26
Chiara/.gitea/workflows/build.yaml
Normal file
|
@ -0,0 +1,26 @@
|
|||
name: Build Docker Image
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
Build-Martina:
|
||||
runs-on: archlinux
|
||||
steps:
|
||||
- uses: https://git.rrricardo.top/actions/checkout@v4
|
||||
name: Check out code
|
||||
- name: Cache nuget packages
|
||||
uses: https://git.rrricardo.top/actions/cache@v4
|
||||
with:
|
||||
path: ~/.nuget/packages
|
||||
key: ${{ runner.os }}-nuget
|
||||
save-always: true
|
||||
- name: Publish dotnet app
|
||||
run: |
|
||||
cd Chiara
|
||||
dotnet publish
|
||||
- name: Build docker image
|
||||
run: |
|
||||
cd Chiara
|
||||
docker build . -t chiara:latest
|
484
Chiara/.gitignore
vendored
Normal file
484
Chiara/.gitignore
vendored
Normal file
|
@ -0,0 +1,484 @@
|
|||
## Ignore Visual Studio temporary files, build results, and
|
||||
## files generated by popular Visual Studio add-ons.
|
||||
##
|
||||
## Get latest from `dotnet new gitignore`
|
||||
|
||||
# dotenv files
|
||||
.env
|
||||
|
||||
# User-specific files
|
||||
*.rsuser
|
||||
*.suo
|
||||
*.user
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
# User-specific files (MonoDevelop/Xamarin Studio)
|
||||
*.userprefs
|
||||
|
||||
# Mono auto generated files
|
||||
mono_crash.*
|
||||
|
||||
# Build results
|
||||
[Dd]ebug/
|
||||
[Dd]ebugPublic/
|
||||
[Rr]elease/
|
||||
[Rr]eleases/
|
||||
x64/
|
||||
x86/
|
||||
[Ww][Ii][Nn]32/
|
||||
[Aa][Rr][Mm]/
|
||||
[Aa][Rr][Mm]64/
|
||||
bld/
|
||||
[Bb]in/
|
||||
[Oo]bj/
|
||||
[Ll]og/
|
||||
[Ll]ogs/
|
||||
|
||||
# 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
|
||||
nunit-*.xml
|
||||
|
||||
# Build Results of an ATL Project
|
||||
[Dd]ebugPS/
|
||||
[Rr]eleasePS/
|
||||
dlldata.c
|
||||
|
||||
# Benchmark Results
|
||||
BenchmarkDotNet.Artifacts/
|
||||
|
||||
# .NET
|
||||
project.lock.json
|
||||
project.fragment.lock.json
|
||||
artifacts/
|
||||
|
||||
# Tye
|
||||
.tye/
|
||||
|
||||
# ASP.NET Scaffolding
|
||||
ScaffoldingReadMe.txt
|
||||
|
||||
# StyleCop
|
||||
StyleCopReport.xml
|
||||
|
||||
# Files built by Visual Studio
|
||||
*_i.c
|
||||
*_p.c
|
||||
*_h.h
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.iobj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.ipdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.tmp_proj
|
||||
*_wpftmp.csproj
|
||||
*.log
|
||||
*.tlog
|
||||
*.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
|
||||
|
||||
# TeamCity is a build add-in
|
||||
_TeamCity*
|
||||
|
||||
# DotCover is a Code Coverage Tool
|
||||
*.dotCover
|
||||
|
||||
# AxoCover is a Code Coverage Tool
|
||||
.axoCover/*
|
||||
!.axoCover/settings.json
|
||||
|
||||
# Coverlet is a free, cross platform Code Coverage Tool
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
coverage*.info
|
||||
|
||||
# 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
|
||||
# NuGet Symbol Packages
|
||||
*.snupkg
|
||||
# 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
|
||||
*.appxbundle
|
||||
*.appxupload
|
||||
|
||||
# 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
|
||||
*- [Bb]ackup.rdl
|
||||
*- [Bb]ackup ([0-9]).rdl
|
||||
*- [Bb]ackup ([0-9][0-9]).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 6 auto-generated project file (contains which files were open etc.)
|
||||
*.vbp
|
||||
|
||||
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
|
||||
*.dsw
|
||||
*.dsp
|
||||
|
||||
# Visual Studio 6 technical files
|
||||
*.ncb
|
||||
*.aps
|
||||
|
||||
# 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/
|
||||
|
||||
# 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/
|
||||
|
||||
# Visual Studio History (VSHistory) files
|
||||
.vshistory/
|
||||
|
||||
# BeatPulse healthcheck temp database
|
||||
healthchecksdb
|
||||
|
||||
# Backup folder for Package Reference Convert tool in Visual Studio 2017
|
||||
MigrationBackup/
|
||||
|
||||
# Ionide (cross platform F# VS Code tools) working folder
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
|
||||
# VS Code files for those working on multiple tools
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
*.code-workspace
|
||||
|
||||
# Local History for Visual Studio Code
|
||||
.history/
|
||||
|
||||
# Windows Installer files from build outputs
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# JetBrains Rider
|
||||
*.sln.iml
|
||||
.idea
|
||||
|
||||
##
|
||||
## Visual studio for Mac
|
||||
##
|
||||
|
||||
|
||||
# globs
|
||||
Makefile.in
|
||||
*.userprefs
|
||||
*.usertasks
|
||||
config.make
|
||||
config.status
|
||||
aclocal.m4
|
||||
install-sh
|
||||
autom4te.cache/
|
||||
*.tar.gz
|
||||
tarballs/
|
||||
test-results/
|
||||
|
||||
# Mac bundle stuff
|
||||
*.dmg
|
||||
*.app
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
|
||||
# General
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear in the root of a volume
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
|
||||
# Windows thumbnail cache files
|
||||
Thumbs.db
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
|
||||
# Dump file
|
||||
*.stackdump
|
||||
|
||||
# Folder config file
|
||||
[Dd]esktop.ini
|
||||
|
||||
# Recycle Bin used on file shares
|
||||
$RECYCLE.BIN/
|
||||
|
||||
# Windows Installer files
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
|
||||
# Windows shortcuts
|
||||
*.lnk
|
||||
|
||||
# Vim temporary swap files
|
||||
*.swp
|
96
Chiara/AnitomySharp/Anitomy.cs
Normal file
96
Chiara/AnitomySharp/Anitomy.cs
Normal file
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// A library capable of parsing Anime filenames
|
||||
/// This code is a C++ to C# port of <see href="https://github.com/erengy/anitomy">Anitomy</see>>,
|
||||
/// using the already existing Java port <see href="https://github.com/Vorror/anitomyJ">AnitomyJ</see> as a reference.
|
||||
/// </summary>
|
||||
public class Anitomy
|
||||
{
|
||||
private Anitomy() {}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an anime <see cref="filename"/> into its constituent elements.
|
||||
/// </summary>
|
||||
/// <param name="filename">the anime file name</param>
|
||||
/// <param name="options">the options to parse with, use Parse(filename) to use default options</param>
|
||||
/// <returns>the list of parsed elements</returns>
|
||||
public static IEnumerable<Element> Parse(string filename, Options options)
|
||||
{
|
||||
List<Element> elements = [];
|
||||
List<Token> tokens = [];
|
||||
|
||||
if (options.ParseFileExtension)
|
||||
{
|
||||
string extension = "";
|
||||
if (RemoveExtensionFromFilename(ref filename, ref extension))
|
||||
{
|
||||
elements.Add(new Element(ElementCategory.ElementFileExtension, extension));
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(filename))
|
||||
{
|
||||
return elements;
|
||||
}
|
||||
elements.Add(new Element(ElementCategory.ElementFileName, filename));
|
||||
|
||||
bool isTokenized = new Tokenizer(filename, elements, options, tokens).Tokenize();
|
||||
if (!isTokenized)
|
||||
{
|
||||
return elements;
|
||||
}
|
||||
new Parser(elements, options, tokens).Parse();
|
||||
return elements;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses an anime <see cref="filename"/> into its consituent elements.
|
||||
/// </summary>
|
||||
/// <param name="filename">the anime file name</param>
|
||||
/// <returns>the list of parsed elements</returns>
|
||||
public static IEnumerable<Element> Parse(string filename)
|
||||
{
|
||||
return Parse(filename, new Options());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the extension from the <see cref="filename"/>
|
||||
/// </summary>
|
||||
/// <param name="filename">the ref that will be updated with the new filename</param>
|
||||
/// <param name="extension">the ref that will be updated with the file extension</param>
|
||||
/// <returns>if the extension was successfully separated from the filename</returns>
|
||||
private static bool RemoveExtensionFromFilename(ref string filename, ref string extension)
|
||||
{
|
||||
int position;
|
||||
if (string.IsNullOrEmpty(filename) || (position = filename.LastIndexOf('.')) == -1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
extension = filename[(position + 1)..];
|
||||
if (extension.Length > 4 || !extension.All(char.IsLetterOrDigit))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string keyword = KeywordManager.Normalize(extension);
|
||||
if (!KeywordManager.Contains(ElementCategory.ElementFileExtension, keyword))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
filename = filename[..position];
|
||||
return true;
|
||||
}
|
||||
}
|
9
Chiara/AnitomySharp/AnitomySharp.csproj
Normal file
9
Chiara/AnitomySharp/AnitomySharp.csproj
Normal file
|
@ -0,0 +1,9 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
90
Chiara/AnitomySharp/Element.cs
Normal file
90
Chiara/AnitomySharp/Element.cs
Normal file
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// An <see cref="Element"/> represents an identified Anime <see cref="Token"/>.
|
||||
/// A single filename may contain multiple of the same
|
||||
/// token(e.g <see cref="ElementCategory.ElementEpisodeNumber"/>).
|
||||
/// </summary>
|
||||
public class Element
|
||||
{
|
||||
public ElementCategory Category { get; set; }
|
||||
public string Value { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new Element
|
||||
/// </summary>
|
||||
/// <param name="category">the category of the element</param>
|
||||
/// <param name="value">the element's value</param>
|
||||
public Element(ElementCategory category, string value)
|
||||
{
|
||||
Category = category;
|
||||
Value = value;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return -1926371015 + Value.GetHashCode();
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
if (this == obj)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (obj == null || GetType() != obj.GetType())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var other = (Element) obj;
|
||||
return Category.Equals(other.Category);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Element{{category={Category}, value='{Value}'}}";
|
||||
}
|
||||
}
|
||||
|
||||
/** Element Categories */
|
||||
public enum ElementCategory
|
||||
{
|
||||
ElementAnimeSeason,
|
||||
ElementAnimeSeasonPrefix,
|
||||
ElementAnimeTitle,
|
||||
ElementAnimeType,
|
||||
ElementAnimeYear,
|
||||
ElementAudioTerm,
|
||||
ElementDeviceCompatibility,
|
||||
ElementEpisodeNumber,
|
||||
ElementEpisodeNumberAlt,
|
||||
ElementEpisodePrefix,
|
||||
ElementEpisodeTitle,
|
||||
ElementFileChecksum,
|
||||
ElementFileExtension,
|
||||
ElementFileName,
|
||||
ElementLanguage,
|
||||
ElementOther,
|
||||
ElementReleaseGroup,
|
||||
ElementReleaseInformation,
|
||||
ElementReleaseVersion,
|
||||
ElementSource,
|
||||
ElementSubtitles,
|
||||
ElementVideoResolution,
|
||||
ElementVideoTerm,
|
||||
ElementVolumeNumber,
|
||||
ElementVolumePrefix,
|
||||
ElementUnknown
|
||||
}
|
279
Chiara/AnitomySharp/Keyword.cs
Normal file
279
Chiara/AnitomySharp/Keyword.cs
Normal file
|
@ -0,0 +1,279 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// A class to manager the list of known anime keywords. This class is analogous to <code>keyword.cpp</code> of Anitomy, and <code>KeywordManager.java</code> of AnitomyJ
|
||||
/// </summary>
|
||||
public static class KeywordManager
|
||||
{
|
||||
private static readonly Dictionary<string, Keyword> s_keys = new();
|
||||
private static readonly Dictionary<string, Keyword> s_extensions = new();
|
||||
private static readonly List<Tuple<ElementCategory, List<string>>> s_peekEntries;
|
||||
|
||||
static KeywordManager()
|
||||
{
|
||||
var optionsDefault = new KeywordOptions();
|
||||
var optionsInvalid = new KeywordOptions(true, true, false);
|
||||
var optionsUnidentifiable = new KeywordOptions(false, true, true);
|
||||
var optionsUnidentifiableInvalid = new KeywordOptions(false, true, false);
|
||||
var optionsUnidentifiableUnsearchable = new KeywordOptions(false, false, true);
|
||||
|
||||
Add(ElementCategory.ElementAnimeSeasonPrefix,
|
||||
optionsUnidentifiable,
|
||||
new List<string> {"SAISON", "SEASON"});
|
||||
|
||||
Add(ElementCategory.ElementAnimeType,
|
||||
optionsUnidentifiable,
|
||||
new List<string> {"GEKIJOUBAN", "MOVIE", "OAD", "OAV", "ONA", "OVA", "SPECIAL", "SPECIALS", "TV"});
|
||||
|
||||
Add(ElementCategory.ElementAnimeType,
|
||||
optionsUnidentifiableUnsearchable,
|
||||
new List<string> {"SP"}); // e.g. "Yumeiro Patissiere SP Professional"
|
||||
|
||||
Add(ElementCategory.ElementAnimeType,
|
||||
optionsUnidentifiableInvalid,
|
||||
new List<string> {"ED", "ENDING", "NCED", "NCOP", "OP", "OPENING", "PREVIEW", "PV"});
|
||||
|
||||
Add(ElementCategory.ElementAudioTerm,
|
||||
optionsDefault,
|
||||
new List<string> {
|
||||
// Audio channels
|
||||
"2.0CH", "2CH", "5.1", "5.1CH", "DTS", "DTS-ES", "DTS5.1",
|
||||
"TRUEHD5.1",
|
||||
// Audio codec
|
||||
"AAC", "AACX2", "AACX3", "AACX4", "AC3", "EAC3", "E-AC-3",
|
||||
"FLAC", "FLACX2", "FLACX3", "FLACX4", "LOSSLESS", "MP3", "OGG", "VORBIS",
|
||||
// Audio language
|
||||
"DUALAUDIO", "DUAL AUDIO"
|
||||
});
|
||||
|
||||
Add(ElementCategory.ElementDeviceCompatibility,
|
||||
optionsDefault,
|
||||
new List<string> {"IPAD3", "IPHONE5", "IPOD", "PS3", "XBOX", "XBOX360"});
|
||||
|
||||
Add(ElementCategory.ElementDeviceCompatibility,
|
||||
optionsUnidentifiable,
|
||||
new List<string> {"ANDROID"});
|
||||
|
||||
Add(ElementCategory.ElementEpisodePrefix,
|
||||
optionsDefault,
|
||||
new List<string> {"EP", "EP.", "EPS", "EPS.", "EPISODE", "EPISODE.", "EPISODES", "CAPITULO", "EPISODIO", "FOLGE"});
|
||||
|
||||
Add(ElementCategory.ElementEpisodePrefix,
|
||||
optionsInvalid,
|
||||
new List<string> {"E", "\\x7B2C"}); // single-letter episode keywords are not valid tokens
|
||||
|
||||
Add(ElementCategory.ElementFileExtension,
|
||||
optionsDefault,
|
||||
new List<string> {"3GP", "AVI", "DIVX", "FLV", "M2TS", "MKV", "MOV", "MP4", "MPG", "OGM", "RM", "RMVB", "TS", "WEBM", "WMV"});
|
||||
|
||||
Add(ElementCategory.ElementFileExtension,
|
||||
optionsInvalid,
|
||||
new List<string> {"AAC", "AIFF", "FLAC", "M4A", "MP3", "MKA", "OGG", "WAV", "WMA", "7Z", "RAR", "ZIP", "ASS", "SRT"});
|
||||
|
||||
Add(ElementCategory.ElementLanguage,
|
||||
optionsDefault,
|
||||
new List<string> {"ENG", "ENGLISH", "ESPANO", "JAP", "PT-BR", "SPANISH", "VOSTFR"});
|
||||
|
||||
Add(ElementCategory.ElementLanguage,
|
||||
optionsUnidentifiable,
|
||||
new List<string> {"ESP", "ITA"}); // e.g. "Tokyo ESP:, "Bokura ga Ita"
|
||||
|
||||
Add(ElementCategory.ElementOther,
|
||||
optionsDefault,
|
||||
new List<string> {"REMASTER", "REMASTERED", "UNCENSORED", "UNCUT", "TS", "VFR", "WIDESCREEN", "WS"});
|
||||
|
||||
Add(ElementCategory.ElementReleaseGroup,
|
||||
optionsDefault,
|
||||
new List<string> {"THORA"});
|
||||
|
||||
Add(ElementCategory.ElementReleaseInformation,
|
||||
optionsDefault,
|
||||
new List<string> {"BATCH", "COMPLETE", "PATCH", "REMUX"});
|
||||
|
||||
Add(ElementCategory.ElementReleaseInformation,
|
||||
optionsUnidentifiable,
|
||||
new List<string> {"END", "FINAL"}); // e.g. "The End of Evangelion", 'Final Approach"
|
||||
|
||||
Add(ElementCategory.ElementReleaseVersion,
|
||||
optionsDefault,
|
||||
new List<string> {"V0", "V1", "V2", "V3", "V4"});
|
||||
|
||||
Add(ElementCategory.ElementSource,
|
||||
optionsDefault,
|
||||
new List<string> {"BD", "BDRIP", "BLURAY", "BLU-RAY", "DVD", "DVD5", "DVD9", "DVD-R2J", "DVDRIP", "DVD-RIP", "R2DVD", "R2J", "R2JDVD", "R2JDVDRIP", "HDTV", "HDTVRIP", "TVRIP", "TV-RIP", "WEBCAST", "WEBRIP"});
|
||||
|
||||
Add(ElementCategory.ElementSubtitles,
|
||||
optionsDefault,
|
||||
new List<string> {"ASS", "BIG5", "DUB", "DUBBED", "HARDSUB", "HARDSUBS", "RAW", "SOFTSUB", "SOFTSUBS", "SUB", "SUBBED", "SUBTITLED"});
|
||||
|
||||
Add(ElementCategory.ElementVideoTerm,
|
||||
optionsDefault,
|
||||
new List<string> {
|
||||
// Frame rate
|
||||
"23.976FPS", "24FPS", "29.97FPS", "30FPS", "60FPS", "120FPS",
|
||||
// Video codec
|
||||
"8BIT", "8-BIT", "10BIT", "10BITS", "10-BIT", "10-BITS",
|
||||
"HI10", "HI10P", "HI444", "HI444P", "HI444PP",
|
||||
"H264", "H265", "H.264", "H.265", "X264", "X265", "X.264",
|
||||
"AVC", "HEVC", "HEVC2", "DIVX", "DIVX5", "DIVX6", "XVID",
|
||||
// Video format
|
||||
"AVI", "RMVB", "WMV", "WMV3", "WMV9",
|
||||
// Video quality
|
||||
"HQ", "LQ",
|
||||
// Video resolution
|
||||
"HD", "SD"});
|
||||
|
||||
Add(ElementCategory.ElementVolumePrefix,
|
||||
optionsDefault,
|
||||
new List<string> {"VOL", "VOL.", "VOLUME"});
|
||||
|
||||
s_peekEntries = new List<Tuple<ElementCategory, List<string>>>
|
||||
{
|
||||
Tuple.Create(ElementCategory.ElementAudioTerm, new List<string> { "Dual Audio" }),
|
||||
Tuple.Create(ElementCategory.ElementVideoTerm, new List<string> { "H264", "H.264", "h264", "h.264" }),
|
||||
Tuple.Create(ElementCategory.ElementVideoResolution, new List<string> { "480p", "720p", "1080p" }),
|
||||
Tuple.Create(ElementCategory.ElementSource, new List<string> { "Blu-Ray" })
|
||||
};
|
||||
}
|
||||
|
||||
public static string Normalize(string word)
|
||||
{
|
||||
return string.IsNullOrEmpty(word) ? word : word.ToUpperInvariant();
|
||||
}
|
||||
|
||||
public static bool Contains(ElementCategory category, string keyword)
|
||||
{
|
||||
var keys = GetKeywordContainer(category);
|
||||
if (keys.TryGetValue(keyword, out var foundEntry))
|
||||
{
|
||||
return foundEntry.Category == category;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds a particular <code>keyword</code>. If found sets <code>category</code> and <code>options</code> to the found search result.
|
||||
/// </summary>
|
||||
/// <param name="keyword">the keyword to search for</param>
|
||||
/// <param name="category">the reference that will be set/changed to the found keyword category</param>
|
||||
/// <param name="options">the reference that will be set/changed to the found keyword options</param>
|
||||
/// <returns>if the keyword was found</returns>
|
||||
public static bool FindAndSet(string keyword, ref ElementCategory category, ref KeywordOptions options)
|
||||
{
|
||||
Dictionary<string, Keyword> keys = GetKeywordContainer(category);
|
||||
if (!keys.TryGetValue(keyword, out var foundEntry))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (category == ElementCategory.ElementUnknown)
|
||||
{
|
||||
category = foundEntry.Category;
|
||||
}
|
||||
else if (foundEntry.Category != category)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
options = foundEntry.Options;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a particular <code>filename</code> and <code>range</code> attempt to preidentify the token before we attempt the main parsing logic
|
||||
/// </summary>
|
||||
/// <param name="filename">the filename</param>
|
||||
/// <param name="range">the search range</param>
|
||||
/// <param name="elements">elements array that any pre-identified elements will be added to</param>
|
||||
/// <param name="preIdentifiedTokens">elements array that any pre-identified token ranges will be added to</param>
|
||||
public static void PeekAndAdd(string filename, TokenRange range, List<Element> elements, List<TokenRange> preIdentifiedTokens)
|
||||
{
|
||||
int endR = range.Offset + range.Size;
|
||||
string search = filename.Substring(range.Offset, endR > filename.Length ? filename.Length - range.Offset : endR - range.Offset);
|
||||
foreach (var entry in s_peekEntries)
|
||||
{
|
||||
foreach (string keyword in entry.Item2)
|
||||
{
|
||||
int foundIdx = search.IndexOf(keyword, StringComparison.CurrentCulture);
|
||||
if (foundIdx == -1) continue;
|
||||
foundIdx += range.Offset;
|
||||
elements.Add(new Element(entry.Item1, keyword));
|
||||
preIdentifiedTokens.Add(new TokenRange(foundIdx, keyword.Length));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private API
|
||||
|
||||
/** Returns the appropriate keyword container. */
|
||||
private static Dictionary<string, Keyword> GetKeywordContainer(ElementCategory category)
|
||||
{
|
||||
return category == ElementCategory.ElementFileExtension ? s_extensions : s_keys;
|
||||
}
|
||||
|
||||
/// Adds a <code>category</code>, <code>options</code>, and <code>keywords</code> to the internal keywords list.
|
||||
private static void Add(ElementCategory category, KeywordOptions options, IEnumerable<string> keywords)
|
||||
{
|
||||
var keys = GetKeywordContainer(category);
|
||||
foreach (string key in keywords.Where(k => !string.IsNullOrEmpty(k) && !keys.ContainsKey(k)))
|
||||
{
|
||||
keys[key] = new Keyword(category, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keyword options for a particular keyword.
|
||||
/// </summary>
|
||||
public class KeywordOptions
|
||||
{
|
||||
public bool Identifiable { get; }
|
||||
public bool Searchable { get; }
|
||||
public bool Valid { get; }
|
||||
|
||||
public KeywordOptions() : this(true, true, true) {}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new keyword options
|
||||
/// </summary>
|
||||
/// <param name="identifiable">if the token is identifiable</param>
|
||||
/// <param name="searchable">if the token is searchable</param>
|
||||
/// <param name="valid">if the token is valid</param>
|
||||
public KeywordOptions(bool identifiable, bool searchable, bool valid)
|
||||
{
|
||||
Identifiable = identifiable;
|
||||
Searchable = searchable;
|
||||
Valid = valid;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Keyword
|
||||
/// </summary>
|
||||
public struct Keyword
|
||||
{
|
||||
public readonly ElementCategory Category;
|
||||
public readonly KeywordOptions Options;
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new Keyword
|
||||
/// </summary>
|
||||
/// <param name="category">the category of the keyword</param>
|
||||
/// <param name="options">the keyword's options</param>
|
||||
public Keyword(ElementCategory category, KeywordOptions options)
|
||||
{
|
||||
Category = category;
|
||||
Options = options;
|
||||
}
|
||||
}
|
28
Chiara/AnitomySharp/Options.cs
Normal file
28
Chiara/AnitomySharp/Options.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// AnitomySharp search configuration options
|
||||
/// </summary>
|
||||
public class Options(
|
||||
string delimiters = " _.&+,|",
|
||||
bool episode = true,
|
||||
bool title = true,
|
||||
bool extension = true,
|
||||
bool group = true)
|
||||
{
|
||||
public string AllowedDelimiters { get; } = delimiters;
|
||||
public bool ParseEpisodeNumber { get; } = episode;
|
||||
public bool ParseEpisodeTitle { get; } = title;
|
||||
public bool ParseFileExtension { get; } = extension;
|
||||
public bool ParseReleaseGroup { get; } = group;
|
||||
}
|
430
Chiara/AnitomySharp/Parser.cs
Normal file
430
Chiara/AnitomySharp/Parser.cs
Normal file
|
@ -0,0 +1,430 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// Class to classify <see cref="Token"/>s
|
||||
/// </summary>
|
||||
public class Parser
|
||||
{
|
||||
public bool IsEpisodeKeywordsFound { get; private set; }
|
||||
public ParserHelper ParseHelper { get; }
|
||||
public ParserNumber ParseNumber { get; }
|
||||
public List<Element> Elements { get; }
|
||||
public List<Token> Tokens { get; }
|
||||
private Options Options { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new token parser
|
||||
/// </summary>
|
||||
/// <param name="elements">the list where parsed elements will be added</param>
|
||||
/// <param name="options">the parser options</param>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
public Parser(List<Element> elements, Options options, List<Token> tokens)
|
||||
{
|
||||
Elements = elements;
|
||||
Options = options;
|
||||
Tokens = tokens;
|
||||
ParseHelper = new ParserHelper(this);
|
||||
ParseNumber = new ParserNumber(this);
|
||||
}
|
||||
|
||||
/** Begins the parsing process */
|
||||
public bool Parse()
|
||||
{
|
||||
SearchForKeywords();
|
||||
SearchForIsolatedNumbers();
|
||||
|
||||
if (Options.ParseEpisodeNumber)
|
||||
{
|
||||
SearchForEpisodeNumber();
|
||||
}
|
||||
|
||||
SearchForAnimeTitle();
|
||||
|
||||
if (Options.ParseReleaseGroup && Empty(ElementCategory.ElementReleaseGroup))
|
||||
{
|
||||
SearchForReleaseGroup();
|
||||
}
|
||||
|
||||
if (Options.ParseEpisodeTitle && !Empty(ElementCategory.ElementEpisodeNumber))
|
||||
{
|
||||
SearchForEpisodeTitle();
|
||||
}
|
||||
|
||||
ValidateElements();
|
||||
return Empty(ElementCategory.ElementAnimeTitle);
|
||||
}
|
||||
|
||||
/** Search for anime keywords. */
|
||||
private void SearchForKeywords()
|
||||
{
|
||||
for (int i = 0; i < Tokens.Count; i++)
|
||||
{
|
||||
var token = Tokens[i];
|
||||
if (token.Category != Token.TokenCategory.Unknown) continue;
|
||||
|
||||
string word = token.Content;
|
||||
word = word.Trim(" -".ToCharArray());
|
||||
if (string.IsNullOrEmpty(word)) continue;
|
||||
|
||||
// Don't bother if the word is a number that cannot be CRC
|
||||
if (word.Length != 8 && StringHelper.IsNumericString(word)) continue;
|
||||
|
||||
string keyword = KeywordManager.Normalize(word);
|
||||
var category = ElementCategory.ElementUnknown;
|
||||
var options = new KeywordOptions();
|
||||
|
||||
if (KeywordManager.FindAndSet(keyword, ref category, ref options))
|
||||
{
|
||||
if (!Options.ParseReleaseGroup && category == ElementCategory.ElementReleaseGroup) continue;
|
||||
if (!ParserHelper.IsElementCategorySearchable(category) || !options.Searchable) continue;
|
||||
if (ParserHelper.IsElementCategorySingular(category) && !Empty(category)) continue;
|
||||
switch (category)
|
||||
{
|
||||
case ElementCategory.ElementAnimeSeasonPrefix:
|
||||
ParseHelper.CheckAndSetAnimeSeasonKeyword(token, i);
|
||||
continue;
|
||||
case ElementCategory.ElementEpisodePrefix when options.Valid:
|
||||
ParseHelper.CheckExtentKeyword(ElementCategory.ElementEpisodeNumber, i, token);
|
||||
continue;
|
||||
case ElementCategory.ElementReleaseVersion:
|
||||
word = word[1..];
|
||||
break;
|
||||
case ElementCategory.ElementVolumePrefix:
|
||||
ParseHelper.CheckExtentKeyword(ElementCategory.ElementVolumeNumber, i, token);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Empty(ElementCategory.ElementFileChecksum) && ParserHelper.IsCrc32(word))
|
||||
{
|
||||
category = ElementCategory.ElementFileChecksum;
|
||||
}
|
||||
else if (Empty(ElementCategory.ElementVideoResolution) && ParserHelper.IsResolution(word))
|
||||
{
|
||||
category = ElementCategory.ElementVideoResolution;
|
||||
}
|
||||
}
|
||||
|
||||
if (category == ElementCategory.ElementUnknown) continue;
|
||||
Elements.Add(new Element(category, word));
|
||||
if (options.Identifiable)
|
||||
{
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Search for episode number. */
|
||||
private void SearchForEpisodeNumber()
|
||||
{
|
||||
// List all unknown tokens that contain a number
|
||||
var tokens = new List<int>();
|
||||
for (int i = 0; i < Tokens.Count; i++)
|
||||
{
|
||||
var token = Tokens[i];
|
||||
if (token.Category == Token.TokenCategory.Unknown &&
|
||||
ParserHelper.IndexOfFirstDigit(token.Content) != -1)
|
||||
{
|
||||
tokens.Add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (tokens.Count == 0) return;
|
||||
|
||||
IsEpisodeKeywordsFound = !Empty(ElementCategory.ElementEpisodeNumber);
|
||||
|
||||
// If a token matches a known episode pattern, it has to be the episode number
|
||||
if (ParseNumber.SearchForEpisodePatterns(tokens)) return;
|
||||
|
||||
// We have previously found an episode number via keywords
|
||||
if (!Empty(ElementCategory.ElementEpisodeNumber)) return;
|
||||
|
||||
// From now on, we're only interested in numeric tokens
|
||||
tokens.RemoveAll(r => !StringHelper.IsNumericString(Tokens[r].Content));
|
||||
|
||||
// e.g. "01 (176)", "29 (04)"
|
||||
if (ParseNumber.SearchForEquivalentNumbers(tokens)) return;
|
||||
|
||||
// e.g. " - 08"
|
||||
if (ParseNumber.SearchForSeparatedNumbers(tokens)) return;
|
||||
|
||||
// "e.g. "[12]", "(2006)"
|
||||
if (ParseNumber.SearchForIsolatedNumbers(tokens)) return;
|
||||
|
||||
// Consider using the last number as a last resort
|
||||
ParseNumber.SearchForLastNumber(tokens);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search for anime title
|
||||
/// </summary>
|
||||
private void SearchForAnimeTitle()
|
||||
{
|
||||
bool enclosedTitle = false;
|
||||
|
||||
int tokenBegin = Token.FindToken(Tokens, 0, Tokens.Count, Token.TokenFlag.FlagNotEnclosed,
|
||||
Token.TokenFlag.FlagUnknown);
|
||||
|
||||
// If that doesn't work, find the first unknown token in the second enclosed
|
||||
// group, assuming that the first one is the release group
|
||||
if (!Token.InListRange(tokenBegin, Tokens))
|
||||
{
|
||||
tokenBegin = 0;
|
||||
enclosedTitle = true;
|
||||
bool skippedPreviousGroup = false;
|
||||
|
||||
do
|
||||
{
|
||||
tokenBegin = Token.FindToken(Tokens, tokenBegin, Tokens.Count, Token.TokenFlag.FlagUnknown);
|
||||
if (!Token.InListRange(tokenBegin, Tokens)) break;
|
||||
|
||||
// Ignore groups that are composed of non-Latin characters
|
||||
if (StringHelper.IsMostlyLatinString(Tokens[tokenBegin].Content) && skippedPreviousGroup)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Get the first unknown token of the next group
|
||||
tokenBegin = Token.FindToken(Tokens, tokenBegin, Tokens.Count, Token.TokenFlag.FlagBracket);
|
||||
tokenBegin = Token.FindToken(Tokens, tokenBegin, Tokens.Count, Token.TokenFlag.FlagUnknown);
|
||||
skippedPreviousGroup = true;
|
||||
} while (Token.InListRange(tokenBegin, Tokens));
|
||||
}
|
||||
|
||||
if (!Token.InListRange(tokenBegin, Tokens)) return;
|
||||
|
||||
// Continue until an identifier (or a bracket, if the title is enclosed) is found
|
||||
int tokenEnd = Token.FindToken(
|
||||
Tokens,
|
||||
tokenBegin,
|
||||
Tokens.Count,
|
||||
Token.TokenFlag.FlagIdentifier,
|
||||
enclosedTitle ? Token.TokenFlag.FlagBracket : Token.TokenFlag.FlagNone);
|
||||
|
||||
// If within the interval there's an open bracket without its matching pair,
|
||||
// move the upper endpoint back to the bracket
|
||||
if (!enclosedTitle)
|
||||
{
|
||||
int lastBracket = tokenEnd;
|
||||
bool bracketOpen = false;
|
||||
for (int i = tokenBegin; i < tokenEnd; i++)
|
||||
{
|
||||
if (Tokens[i].Category != Token.TokenCategory.Bracket) continue;
|
||||
lastBracket = i;
|
||||
bracketOpen = !bracketOpen;
|
||||
}
|
||||
|
||||
if (bracketOpen) tokenEnd = lastBracket;
|
||||
}
|
||||
|
||||
// If the interval ends with an enclosed group (e.g. "Anime Title [Fansub]"),
|
||||
// move the upper endpoint back to the beginning of the group. We ignore
|
||||
// parentheses in order to keep certain groups (e.g. "(TV)") intact.
|
||||
if (!enclosedTitle)
|
||||
{
|
||||
int token = Token.FindPrevToken(Tokens, tokenEnd, Token.TokenFlag.FlagNotDelimiter);
|
||||
|
||||
while (ParseHelper.IsTokenCategory(token, Token.TokenCategory.Bracket) && Tokens[token].Content[0] != ')')
|
||||
{
|
||||
token = Token.FindPrevToken(Tokens, token, Token.TokenFlag.FlagBracket);
|
||||
if (!Token.InListRange(token, Tokens)) continue;
|
||||
tokenEnd = token;
|
||||
token = Token.FindPrevToken(Tokens, tokenEnd, Token.TokenFlag.FlagNotDelimiter);
|
||||
}
|
||||
}
|
||||
|
||||
ParseHelper.BuildElement(ElementCategory.ElementAnimeTitle, false,
|
||||
Tokens.GetRange(tokenBegin, tokenEnd - tokenBegin));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search for release group
|
||||
/// </summary>
|
||||
private void SearchForReleaseGroup()
|
||||
{
|
||||
for (int tokenBegin = 0, tokenEnd = tokenBegin; tokenBegin < Tokens.Count;)
|
||||
{
|
||||
// Find the first enclosed unknown token
|
||||
tokenBegin = Token.FindToken(Tokens, tokenEnd, Tokens.Count, Token.TokenFlag.FlagEnclosed,
|
||||
Token.TokenFlag.FlagUnknown);
|
||||
if (!Token.InListRange(tokenBegin, Tokens)) return;
|
||||
|
||||
// Continue until a bracket or identifier is found
|
||||
tokenEnd = Token.FindToken(Tokens, tokenBegin, Tokens.Count, Token.TokenFlag.FlagBracket,
|
||||
Token.TokenFlag.FlagIdentifier);
|
||||
if (!Token.InListRange(tokenEnd, Tokens) ||
|
||||
Tokens[tokenEnd].Category != Token.TokenCategory.Bracket) continue;
|
||||
|
||||
// Ignore if it's not the first non-delimiter token in group
|
||||
int prevToken = Token.FindPrevToken(Tokens, tokenBegin, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (Token.InListRange(prevToken, Tokens) &&
|
||||
Tokens[prevToken].Category != Token.TokenCategory.Bracket) continue;
|
||||
|
||||
ParseHelper.BuildElement(ElementCategory.ElementReleaseGroup, true,
|
||||
Tokens.GetRange(tokenBegin, tokenEnd - tokenBegin));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search for episode title
|
||||
/// </summary>
|
||||
private void SearchForEpisodeTitle()
|
||||
{
|
||||
int tokenBegin;
|
||||
int tokenEnd = 0;
|
||||
|
||||
do
|
||||
{
|
||||
// Find the first non-enclosed unknown token
|
||||
tokenBegin = Token.FindToken(Tokens, tokenEnd, Tokens.Count, Token.TokenFlag.FlagNotEnclosed,
|
||||
Token.TokenFlag.FlagUnknown);
|
||||
if (!Token.InListRange(tokenBegin, Tokens)) return;
|
||||
|
||||
// Continue until a bracket or identifier is found
|
||||
tokenEnd = Token.FindToken(Tokens, tokenBegin, Tokens.Count, Token.TokenFlag.FlagBracket,
|
||||
Token.TokenFlag.FlagIdentifier);
|
||||
|
||||
// Ignore if it's only a dash
|
||||
if (tokenEnd - tokenBegin <= 2 && ParserHelper.IsDashCharacter(Tokens[tokenBegin].Content[0])) continue;
|
||||
//if (tokenBegin.Pos == null || tokenEnd.Pos == null) continue;
|
||||
ParseHelper.BuildElement(ElementCategory.ElementEpisodeTitle, false,
|
||||
Tokens.GetRange(tokenBegin, tokenEnd - tokenBegin));
|
||||
return;
|
||||
} while (Token.InListRange(tokenBegin, Tokens));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Search for isolated numbers
|
||||
/// </summary>
|
||||
private void SearchForIsolatedNumbers()
|
||||
{
|
||||
for (int i = 0; i < Tokens.Count; i++)
|
||||
{
|
||||
var token = Tokens[i];
|
||||
if (token.Category != Token.TokenCategory.Unknown || !StringHelper.IsNumericString(token.Content) ||
|
||||
!ParseHelper.IsTokenIsolated(i))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int number = StringHelper.StringToInt(token.Content);
|
||||
|
||||
// Anime year
|
||||
if (number >= ParserNumber.AnimeYearMin && number <= ParserNumber.AnimeYearMax)
|
||||
{
|
||||
if (Empty(ElementCategory.ElementAnimeYear))
|
||||
{
|
||||
Elements.Add(new Element(ElementCategory.ElementAnimeYear, token.Content));
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Video resolution
|
||||
if (number != 480 && number != 720 && number != 1080) continue;
|
||||
// If these numbers are isolated, it's more likely for them to be the
|
||||
// video resolution rather than the episode number. Some fansub groups use these without the "p" suffix.
|
||||
if (!Empty(ElementCategory.ElementVideoResolution)) continue;
|
||||
Elements.Add(new Element(ElementCategory.ElementVideoResolution, token.Content));
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validate Elements
|
||||
/// </summary>
|
||||
private void ValidateElements()
|
||||
{
|
||||
if (Empty(ElementCategory.ElementAnimeType) || Empty(ElementCategory.ElementEpisodeTitle))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string episodeTitle = Get(ElementCategory.ElementEpisodeTitle);
|
||||
|
||||
for (int i = 0; i < Elements.Count;)
|
||||
{
|
||||
var el = Elements[i];
|
||||
|
||||
if (el.Category == ElementCategory.ElementAnimeType)
|
||||
{
|
||||
if (episodeTitle.Contains(el.Value))
|
||||
{
|
||||
if (episodeTitle.Length == el.Value.Length)
|
||||
{
|
||||
Elements.RemoveAll(element =>
|
||||
element.Category == ElementCategory.ElementEpisodeTitle); // invalid episode title
|
||||
}
|
||||
else
|
||||
{
|
||||
string keyword = KeywordManager.Normalize(el.Value);
|
||||
if (KeywordManager.Contains(ElementCategory.ElementAnimeType, keyword))
|
||||
{
|
||||
i = Erase(el); // invalid anime type
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
++i;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the parser contains this category
|
||||
/// </summary>
|
||||
/// <param name="category"></param>
|
||||
/// <returns></returns>
|
||||
private bool Empty(ElementCategory category)
|
||||
{
|
||||
return Elements.All(element => element.Category != category);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the value of a particular category
|
||||
/// </summary>
|
||||
/// <param name="category"></param>
|
||||
/// <returns></returns>
|
||||
private string Get(ElementCategory category)
|
||||
{
|
||||
var foundElement = Elements.Find(element => element.Category == category);
|
||||
|
||||
if (foundElement != null) return foundElement.Value;
|
||||
Element e = new Element(category, "");
|
||||
Elements.Add(e);
|
||||
foundElement = e;
|
||||
|
||||
return foundElement.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes the first element with the same <code>element.Category</code> and returns the deleted element's position.
|
||||
/// </summary>
|
||||
private int Erase(Element element)
|
||||
{
|
||||
int removedIdx = -1;
|
||||
for (int i = 0; i < Elements.Count; i++)
|
||||
{
|
||||
var currentElement = Elements[i];
|
||||
if (element.Category != currentElement.Category) continue;
|
||||
removedIdx = i;
|
||||
Elements.RemoveAt(i);
|
||||
break;
|
||||
}
|
||||
|
||||
return removedIdx;
|
||||
}
|
||||
}
|
281
Chiara/AnitomySharp/ParserHelper.cs
Normal file
281
Chiara/AnitomySharp/ParserHelper.cs
Normal file
|
@ -0,0 +1,281 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// Utility class to assist in the parsing.
|
||||
/// </summary>
|
||||
public class ParserHelper(Parser parser)
|
||||
{
|
||||
private static readonly System.Buffers.SearchValues<char> s_myChars = System.Buffers.SearchValues.Create("xX\u00D7");
|
||||
private const string Dashes = "-\u2010\u2011\u2012\u2013\u2014\u2015";
|
||||
private const string DashesWithSpace = " -\u2010\u2011\u2012\u2013\u2014\u2015";
|
||||
|
||||
private static readonly Dictionary<string, string> s_ordinals = new()
|
||||
{
|
||||
{"1st", "1"}, {"First", "1"},
|
||||
{"2nd", "2"}, {"Second", "2"},
|
||||
{"3rd", "3"}, {"Third", "3"},
|
||||
{"4th", "4"}, {"Fourth", "4"},
|
||||
{"5th", "5"}, {"Fifth", "5"},
|
||||
{"6th", "6"}, {"Sixth", "6"},
|
||||
{"7th", "7"}, {"Seventh", "7"},
|
||||
{"8th", "8"}, {"Eighth", "8"},
|
||||
{"9th", "9"}, {"Ninth", "9"}
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>result</code> matches the <code>category</code>.
|
||||
/// </summary>
|
||||
public bool IsTokenCategory(int result, Token.TokenCategory category)
|
||||
{
|
||||
return Token.InListRange(result, parser.Tokens) && parser.Tokens[result].Category == category;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>str</code> is a CRC string.
|
||||
/// </summary>
|
||||
public static bool IsCrc32(string str)
|
||||
{
|
||||
return str.Length == 8 && StringHelper.IsHexadecimalString(str);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>character</code> is a dash character
|
||||
/// </summary>
|
||||
public static bool IsDashCharacter(char c)
|
||||
{
|
||||
return Dashes.Contains(c.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a number from an original (e.g. 2nd)
|
||||
/// </summary>
|
||||
private static string GetNumberFromOrdinal(string str)
|
||||
{
|
||||
return string.IsNullOrEmpty(str) ? string.Empty : s_ordinals.GetValueOrDefault(str, "");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index of the first digit in the <code>str</code>; -1 otherwise.
|
||||
/// </summary>
|
||||
public static int IndexOfFirstDigit(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str)) return -1;
|
||||
for (int i = 0; i < str.Length; i++)
|
||||
{
|
||||
if (char.IsDigit(str, i))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>str</code> is a resolution.
|
||||
/// </summary>
|
||||
public static bool IsResolution(string str)
|
||||
{
|
||||
if (string.IsNullOrEmpty(str)) return false;
|
||||
const int minWidthSize = 3;
|
||||
const int minHeightSize = 3;
|
||||
|
||||
switch (str.Length)
|
||||
{
|
||||
case >= minWidthSize + 1 + minHeightSize:
|
||||
{
|
||||
int pos = str.AsSpan().IndexOfAny(s_myChars);
|
||||
if (pos == -1 || pos < minWidthSize || pos > str.Length - (minHeightSize + 1)) return false;
|
||||
return !str.Where((t, i) => i != pos && !char.IsDigit(t)).Any();
|
||||
}
|
||||
case < minHeightSize + 1:
|
||||
return false;
|
||||
default:
|
||||
{
|
||||
if (char.ToLower(str[^1]) != 'p') return false;
|
||||
for (int i = 0; i < str.Length - 1; i++)
|
||||
{
|
||||
if (!char.IsDigit(str[i])) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>category</code> is searchable.
|
||||
/// </summary>
|
||||
public static bool IsElementCategorySearchable(ElementCategory category)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
ElementCategory.ElementAnimeSeasonPrefix or ElementCategory.ElementAnimeType
|
||||
or ElementCategory.ElementAudioTerm or ElementCategory.ElementDeviceCompatibility
|
||||
or ElementCategory.ElementEpisodePrefix or ElementCategory.ElementFileChecksum
|
||||
or ElementCategory.ElementLanguage or ElementCategory.ElementOther
|
||||
or ElementCategory.ElementReleaseGroup or ElementCategory.ElementReleaseInformation
|
||||
or ElementCategory.ElementReleaseVersion or ElementCategory.ElementSource
|
||||
or ElementCategory.ElementSubtitles or ElementCategory.ElementVideoResolution
|
||||
or ElementCategory.ElementVideoTerm or ElementCategory.ElementVolumePrefix => true,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether the <code>category</code> is singular.
|
||||
/// </summary>
|
||||
public static bool IsElementCategorySingular(ElementCategory category)
|
||||
{
|
||||
return category switch
|
||||
{
|
||||
ElementCategory.ElementAnimeSeason or ElementCategory.ElementAnimeType or ElementCategory.ElementAudioTerm
|
||||
or ElementCategory.ElementDeviceCompatibility or ElementCategory.ElementEpisodeNumber
|
||||
or ElementCategory.ElementLanguage or ElementCategory.ElementOther
|
||||
or ElementCategory.ElementReleaseInformation or ElementCategory.ElementSource
|
||||
or ElementCategory.ElementVideoTerm => false,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not a token at the current <code>pos</code> is isolated(surrounded by braces).
|
||||
/// </summary>
|
||||
public bool IsTokenIsolated(int pos)
|
||||
{
|
||||
int prevToken = Token.FindPrevToken(parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (!IsTokenCategory(prevToken, Token.TokenCategory.Bracket)) return false;
|
||||
int nextToken = Token.FindNextToken(parser.Tokens, pos, Token.TokenFlag.FlagNotDelimiter);
|
||||
return IsTokenCategory(nextToken, Token.TokenCategory.Bracket);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds and sets the anime season keyword.
|
||||
/// </summary>
|
||||
public bool CheckAndSetAnimeSeasonKeyword(Token token, int currentTokenPos)
|
||||
{
|
||||
int previousToken = Token.FindPrevToken(parser.Tokens, currentTokenPos, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (Token.InListRange(previousToken, parser.Tokens))
|
||||
{
|
||||
string number = GetNumberFromOrdinal(parser.Tokens[previousToken].Content);
|
||||
if (!string.IsNullOrEmpty(number))
|
||||
{
|
||||
SetAnimeSeason(parser.Tokens[previousToken], token, number);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
int nextToken = Token.FindNextToken(parser.Tokens, currentTokenPos, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (!Token.InListRange(nextToken, parser.Tokens) ||
|
||||
!StringHelper.IsNumericString(parser.Tokens[nextToken].Content)) return false;
|
||||
SetAnimeSeason(token, parser.Tokens[nextToken], parser.Tokens[nextToken].Content);
|
||||
return true;
|
||||
|
||||
void SetAnimeSeason(Token first, Token second, string content)
|
||||
{
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementAnimeSeason, content));
|
||||
first.Category = Token.TokenCategory.Identifier;
|
||||
second.Category = Token.TokenCategory.Identifier;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A Method to find the correct volume/episode number when prefixed (i.e. Vol.4).
|
||||
/// </summary>
|
||||
/// <param name="category">the category we're searching for</param>
|
||||
/// <param name="currentTokenPos">the current token position</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if we found the volume/episode number</returns>
|
||||
public bool CheckExtentKeyword(ElementCategory category, int currentTokenPos, Token token)
|
||||
{
|
||||
int nToken = Token.FindNextToken(parser.Tokens, currentTokenPos, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (!IsTokenCategory(nToken, Token.TokenCategory.Unknown)) return false;
|
||||
if (IndexOfFirstDigit(parser.Tokens[nToken].Content) != 0) return false;
|
||||
switch (category)
|
||||
{
|
||||
case ElementCategory.ElementEpisodeNumber:
|
||||
if (!parser.ParseNumber.MatchEpisodePatterns(parser.Tokens[nToken].Content, parser.Tokens[nToken]))
|
||||
{
|
||||
parser.ParseNumber.SetEpisodeNumber(parser.Tokens[nToken].Content, parser.Tokens[nToken], false);
|
||||
}
|
||||
break;
|
||||
case ElementCategory.ElementVolumeNumber:
|
||||
if (!parser.ParseNumber.MatchVolumePatterns(parser.Tokens[nToken].Content, parser.Tokens[nToken]))
|
||||
{
|
||||
parser.ParseNumber.SetVolumeNumber(parser.Tokens[nToken].Content, parser.Tokens[nToken], false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
|
||||
public void BuildElement(ElementCategory category, bool keepDelimiters, List<Token> tokens)
|
||||
{
|
||||
var element = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
var token = tokens[i];
|
||||
switch (token.Category)
|
||||
{
|
||||
case Token.TokenCategory.Unknown:
|
||||
element.Append(token.Content);
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
break;
|
||||
case Token.TokenCategory.Bracket:
|
||||
element.Append(token.Content);
|
||||
break;
|
||||
case Token.TokenCategory.Delimiter:
|
||||
string delimiter = "";
|
||||
if (!string.IsNullOrEmpty(token.Content))
|
||||
{
|
||||
delimiter = token.Content[0].ToString();
|
||||
}
|
||||
|
||||
if (keepDelimiters)
|
||||
{
|
||||
element.Append(delimiter);
|
||||
}
|
||||
else if (Token.InListRange(i, tokens))
|
||||
{
|
||||
switch (delimiter)
|
||||
{
|
||||
case ",":
|
||||
case "&":
|
||||
element.Append(delimiter);
|
||||
break;
|
||||
default:
|
||||
element.Append(' ');
|
||||
break;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!keepDelimiters)
|
||||
{
|
||||
element = new StringBuilder(element.ToString().Trim(DashesWithSpace.ToCharArray()));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(element.ToString()))
|
||||
{
|
||||
parser.Elements.Add(new Element(category, element.ToString()));
|
||||
}
|
||||
}
|
||||
}
|
681
Chiara/AnitomySharp/ParserNumber.cs
Normal file
681
Chiara/AnitomySharp/ParserNumber.cs
Normal file
|
@ -0,0 +1,681 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// A utility class to assist in number parsing.
|
||||
/// </summary>
|
||||
public partial class ParserNumber(Parser parser)
|
||||
{
|
||||
public const int AnimeYearMin = 1900;
|
||||
public const int AnimeYearMax = 2100;
|
||||
private const int EpisodeNumberMax = AnimeYearMax - 1;
|
||||
private const int VolumeNumberMax = 50;
|
||||
private const string RegexMatchOnlyStart = @"\A(?:";
|
||||
private const string RegexMatchOnlyEnd = @")\z";
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>number</code> is a volume number
|
||||
/// </summary>
|
||||
private static bool IsValidVolumeNumber(string number)
|
||||
{
|
||||
return StringHelper.StringToInt(number) <= VolumeNumberMax;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>number</code> is a valid episode number.
|
||||
/// </summary>
|
||||
private static bool IsValidEpisodeNumber(string number)
|
||||
{
|
||||
// Eliminate non numeric portion of number, then parse as double.
|
||||
string temp = "";
|
||||
for (int i = 0; i < number.Length && char.IsDigit(number[i]); i++)
|
||||
{
|
||||
temp += number[i];
|
||||
}
|
||||
|
||||
return !string.IsNullOrEmpty(temp) && double.Parse(temp) <= EpisodeNumberMax;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the alternative episode number.
|
||||
/// </summary>
|
||||
private void SetAlternativeEpisodeNumber(string number, Token token)
|
||||
{
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementEpisodeNumberAlt, number));
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the volume number.
|
||||
/// </summary>
|
||||
/// <param name="number">the number</param>
|
||||
/// <param name="token">the token which contains the volume number</param>
|
||||
/// <param name="validate">true if we should check if it's a valid number, false to disable verification</param>
|
||||
/// <returns>true if the volume number was set</returns>
|
||||
public bool SetVolumeNumber(string number, Token token, bool validate)
|
||||
{
|
||||
if (validate && !IsValidVolumeNumber(number)) return false;
|
||||
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementVolumeNumber, number));
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the anime episode number.
|
||||
/// </summary>
|
||||
/// <param name="number">the episode number</param>
|
||||
/// <param name="token">the token which contains the volume number</param>
|
||||
/// <param name="validate">true if we should check if it's a valid episode number; false to disable validation</param>
|
||||
/// <returns>true if the episode number was set</returns>
|
||||
public bool SetEpisodeNumber(string number, Token token, bool validate)
|
||||
{
|
||||
if (validate && !IsValidEpisodeNumber(number)) return false;
|
||||
token.Category = Token.TokenCategory.Identifier;
|
||||
var category = ElementCategory.ElementEpisodeNumber;
|
||||
|
||||
if (parser.IsEpisodeKeywordsFound)
|
||||
{
|
||||
foreach (var element in parser.Elements)
|
||||
{
|
||||
if (element.Category != ElementCategory.ElementEpisodeNumber) continue;
|
||||
|
||||
int comparison = StringHelper.StringToInt(number) - StringHelper.StringToInt(element.Value);
|
||||
switch (comparison)
|
||||
{
|
||||
case > 0:
|
||||
category = ElementCategory.ElementEpisodeNumberAlt;
|
||||
break;
|
||||
case < 0:
|
||||
element.Category = ElementCategory.ElementEpisodeNumberAlt;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
parser.Elements.Add(new Element(category, number));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if a number follows the specified <code>token</code>
|
||||
/// </summary>
|
||||
/// <param name="category">the category to set if a number follows the <code>token</code></param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if a number follows the token; false otherwise</returns>
|
||||
private bool NumberComesAfterPrefix(ElementCategory category, Token token)
|
||||
{
|
||||
int numberBegin = ParserHelper.IndexOfFirstDigit(token.Content);
|
||||
string prefix = StringHelper.SubstringWithCheck(token.Content, 0, numberBegin).ToUpperInvariant();
|
||||
if (!KeywordManager.Contains(category, prefix)) return false;
|
||||
string number = StringHelper.SubstringWithCheck(token.Content, numberBegin, token.Content.Length - numberBegin);
|
||||
|
||||
switch (category)
|
||||
{
|
||||
case ElementCategory.ElementEpisodePrefix:
|
||||
if (!MatchEpisodePatterns(number, token))
|
||||
{
|
||||
SetEpisodeNumber(number, token, false);
|
||||
}
|
||||
return true;
|
||||
case ElementCategory.ElementVolumePrefix:
|
||||
if (!MatchVolumePatterns(number, token))
|
||||
{
|
||||
SetVolumeNumber(number, token, false);
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether the number precedes the word "of"
|
||||
/// </summary>
|
||||
/// <param name="token">the token</param>
|
||||
/// <param name="currentTokenIdx">the index of the token</param>
|
||||
/// <returns>true if the token precedes the word "of"</returns>
|
||||
private bool NumberComesBeforeAnotherNumber(Token token, int currentTokenIdx)
|
||||
{
|
||||
int separatorToken = Token.FindNextToken(parser.Tokens, currentTokenIdx, Token.TokenFlag.FlagNotDelimiter);
|
||||
|
||||
if (!Token.InListRange(separatorToken, parser.Tokens)) return false;
|
||||
var separators = new List<Tuple<string, bool>>
|
||||
{
|
||||
Tuple.Create("&", true),
|
||||
Tuple.Create("of", false)
|
||||
};
|
||||
|
||||
foreach (var separator in separators)
|
||||
{
|
||||
if (parser.Tokens[separatorToken].Content != separator.Item1) continue;
|
||||
int otherToken = Token.FindNextToken(parser.Tokens, separatorToken, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (!Token.InListRange(otherToken, parser.Tokens)
|
||||
|| !StringHelper.IsNumericString(parser.Tokens[otherToken].Content)) continue;
|
||||
SetEpisodeNumber(token.Content, token, false);
|
||||
if (separator.Item2)
|
||||
{
|
||||
SetEpisodeNumber(parser.Tokens[otherToken].Content, parser.Tokens[otherToken], false);
|
||||
}
|
||||
|
||||
parser.Tokens[separatorToken].Category = Token.TokenCategory.Identifier;
|
||||
parser.Tokens[otherToken].Category = Token.TokenCategory.Identifier;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// EPISODE MATCHERS
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find an episode/season inside a <code>word</code>
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the word was matched to an episode/season number</returns>
|
||||
public bool MatchEpisodePatterns(string word, Token token)
|
||||
{
|
||||
if (StringHelper.IsNumericString(word)) return false;
|
||||
|
||||
word = word.Trim(" -".ToCharArray());
|
||||
|
||||
bool numericFront = char.IsDigit(word[0]);
|
||||
bool numericBack = char.IsDigit(word[^1]);
|
||||
|
||||
if (numericFront && numericBack)
|
||||
{
|
||||
// e.g. "01v2"
|
||||
if (MatchSingleEpisodePattern(word, token))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// e.g. "01-02", "03-05v2"
|
||||
|
||||
if (MatchMultiEpisodePattern(word, token))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// e.g. "07.5"
|
||||
|
||||
if (MatchFractionalEpisodePattern(word, token))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (numericBack)
|
||||
{
|
||||
// e.g. "2x01", "S01E03", "S01-02xE001-150"
|
||||
if (MatchSeasonAndEpisodePattern(word, token))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
// e.g. "#01", "#02-03v2"
|
||||
|
||||
if (MatchNumberSignPattern(word, token))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
switch (numericFront)
|
||||
{
|
||||
// e.g. "ED1", "OP4a", "OVA2"
|
||||
case false when MatchTypeAndEpisodePattern(word, token):
|
||||
// e.g. "4a", "111C"
|
||||
case true when !numericBack && MatchPartialEpisodePattern(word, token):
|
||||
return true;
|
||||
default:
|
||||
// U+8A71 is used as counter for stories, episodes of TV series, etc.
|
||||
return numericFront && MatchJapaneseCounterPattern(word, token);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match a single episode pattern. e.g. "01v2".
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchSingleEpisodePattern(string word, Token token)
|
||||
{
|
||||
var match = SingleEpisodeRegex().Match(word);
|
||||
|
||||
if (!match.Success) return false;
|
||||
|
||||
SetEpisodeNumber(match.Groups[1].Value, token, false);
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementReleaseVersion, match.Groups[2].Value));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match a multi episode pattern. e.g. "01-02", "03-05v2".
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchMultiEpisodePattern(string word, Token token)
|
||||
{
|
||||
var match = MultiEpisodeRegex().Match(word);
|
||||
if (!match.Success) return false;
|
||||
|
||||
string lowerBound = match.Groups[1].Value;
|
||||
string upperBound = match.Groups[3].Value;
|
||||
|
||||
if (StringHelper.StringToInt(lowerBound) >= StringHelper.StringToInt(upperBound)) return false;
|
||||
if (!SetEpisodeNumber(lowerBound, token, true)) return false;
|
||||
SetEpisodeNumber(upperBound, token, true);
|
||||
if (!string.IsNullOrEmpty(match.Groups[2].Value))
|
||||
{
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementReleaseVersion, match.Groups[2].Value));
|
||||
}
|
||||
if (!string.IsNullOrEmpty(match.Groups[4].Value))
|
||||
{
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementReleaseVersion, match.Groups[4].Value));
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match season and episode patterns. e.g. "2x01", "S01E03", "S01-02xE001-150".
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchSeasonAndEpisodePattern(string word, Token token)
|
||||
{
|
||||
var match = SeasonEpisodeRegex().Match(word);
|
||||
if (!match.Success) return false;
|
||||
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementAnimeSeason, match.Groups[1].Value));
|
||||
if (!string.IsNullOrEmpty(match.Groups[2].Value))
|
||||
{
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementAnimeSeason, match.Groups[2].Value));
|
||||
}
|
||||
SetEpisodeNumber(match.Groups[3].Value, token, false);
|
||||
if (!string.IsNullOrEmpty(match.Groups[4].Value))
|
||||
{
|
||||
SetEpisodeNumber(match.Groups[4].Value, token, false);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match type and episode. e.g. "ED1", "OP4a", "OVA2".
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchTypeAndEpisodePattern(string word, Token token)
|
||||
{
|
||||
int numberBegin = ParserHelper.IndexOfFirstDigit(word);
|
||||
string prefix = StringHelper.SubstringWithCheck(word, 0, numberBegin);
|
||||
|
||||
var category = ElementCategory.ElementAnimeType;
|
||||
var options = new KeywordOptions();
|
||||
|
||||
if (!KeywordManager.FindAndSet(KeywordManager.Normalize(prefix), ref category, ref options)) return false;
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementAnimeType, prefix));
|
||||
string number = word.Substring(numberBegin);
|
||||
if (!MatchEpisodePatterns(number, token) && !SetEpisodeNumber(number, token, true)) return false;
|
||||
int foundIdx = parser.Tokens.IndexOf(token);
|
||||
if (foundIdx == -1) return true;
|
||||
token.Content = number;
|
||||
parser.Tokens.Insert(foundIdx,
|
||||
new Token(options.Identifiable ? Token.TokenCategory.Identifier : Token.TokenCategory.Unknown, prefix, token.Enclosed));
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match fractional episodes. e.g. "07.5"
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchFractionalEpisodePattern(string word, Token token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word))
|
||||
{
|
||||
word = "";
|
||||
}
|
||||
|
||||
const string regexPattern = RegexMatchOnlyStart + @"\d+\.5" + RegexMatchOnlyEnd;
|
||||
var match = Regex.Match(word, regexPattern);
|
||||
return match.Success && SetEpisodeNumber(word, token, true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match partial episodes. e.g. "4a", "111C".
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchPartialEpisodePattern(string word, Token token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word)) return false;
|
||||
int foundIdx = Enumerable.Range(0, word.Length)
|
||||
.DefaultIfEmpty(word.Length)
|
||||
.FirstOrDefault(value => !char.IsDigit(word[value]));
|
||||
int suffixLength = word.Length - foundIdx;
|
||||
|
||||
return suffixLength == 1 && IsValidSuffix(word[foundIdx]) && SetEpisodeNumber(word, token, true);
|
||||
|
||||
bool IsValidSuffix(int c) => c is >= 'A' and <= 'C' or >= 'a' and <= 'c';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match episodes with number signs. e.g. "#01", "#02-03v2"
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchNumberSignPattern(string word, Token token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word) || word[0] != '#') word = "";
|
||||
var match = NumberEpisodeRegex().Match(word);
|
||||
if (!match.Success) return false;
|
||||
|
||||
if (!SetEpisodeNumber(match.Groups[1].Value, token, true)) return false;
|
||||
if (!string.IsNullOrEmpty(match.Groups[2].Value))
|
||||
{
|
||||
SetEpisodeNumber(match.Groups[2].Value, token, false);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(match.Groups[3].Value))
|
||||
{
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementReleaseVersion, match.Groups[3].Value));
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match Japanese patterns. e.g. U+8A71 is used as counter for stories, episodes of TV series, etc.
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchJapaneseCounterPattern(string word, Token token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word) || word[^1] != '\u8A71') return false;
|
||||
var match = JapaneseEpisodeRegex().Match(word);
|
||||
if (!match.Success) return false;
|
||||
SetEpisodeNumber(match.Groups[1].Value, token, false);
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
// VOLUME MATCHES
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to find an episode/season inside a <code>word</code>
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the word was matched to an episode/season number</returns>
|
||||
public bool MatchVolumePatterns(string word, Token token)
|
||||
{
|
||||
// All patterns contain at least one non-numeric character
|
||||
if (StringHelper.IsNumericString(word)) return false;
|
||||
|
||||
word = word.Trim(" -".ToCharArray());
|
||||
|
||||
bool numericFront = char.IsDigit(word[0]);
|
||||
bool numericBack = char.IsDigit(word[^1]);
|
||||
|
||||
if (numericFront && numericBack)
|
||||
{
|
||||
// e.g. "01v2" e.g. "01-02", "03-05v2"
|
||||
return MatchSingleVolumePattern(word, token) || MatchMultiVolumePattern(word, token);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match single volume. e.g. "01v2"
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchSingleVolumePattern(string word, Token token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word)) word = "";
|
||||
var match = SingleVolumeRegex().Match(word);
|
||||
if (!match.Success) return false;
|
||||
|
||||
SetVolumeNumber(match.Groups[1].Value, token, false);
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementReleaseVersion, match.Groups[2].Value));
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Match multi-volume. e.g. "01-02", "03-05v2".
|
||||
/// </summary>
|
||||
/// <param name="word">the word</param>
|
||||
/// <param name="token">the token</param>
|
||||
/// <returns>true if the token matched</returns>
|
||||
private bool MatchMultiVolumePattern(string word, Token token)
|
||||
{
|
||||
if (string.IsNullOrEmpty(word)) word = "";
|
||||
var match = MultiVolumeRegex().Match(word);
|
||||
if (!match.Success) return false;
|
||||
|
||||
string lowerBound = match.Groups[1].Value;
|
||||
string upperBound = match.Groups[2].Value;
|
||||
if (StringHelper.StringToInt(lowerBound) >= StringHelper.StringToInt(upperBound)) return false;
|
||||
if (!SetVolumeNumber(lowerBound, token, true)) return false;
|
||||
SetVolumeNumber(upperBound, token, false);
|
||||
if (string.IsNullOrEmpty(match.Groups[3].Value))
|
||||
{
|
||||
parser.Elements.Add(new Element(ElementCategory.ElementReleaseVersion, match.Groups[3].Value));
|
||||
}
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
// SEARCH
|
||||
|
||||
/// <summary>
|
||||
/// Searches for isolated numbers in a list of <code>tokens</code>.
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
/// <returns>true if an isolated number was found</returns>
|
||||
public bool SearchForIsolatedNumbers(IEnumerable<int> tokens)
|
||||
{
|
||||
return tokens
|
||||
.Where(it => parser.Tokens[it].Enclosed && parser.ParseHelper.IsTokenIsolated(it))
|
||||
.Any(it => SetEpisodeNumber(parser.Tokens[it].Content, parser.Tokens[it], true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for separated numbers in a list of <code>tokens</code>.
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
/// <returns>true fi a separated number was found</returns>
|
||||
public bool SearchForSeparatedNumbers(List<int> tokens)
|
||||
{
|
||||
foreach (int it in tokens)
|
||||
{
|
||||
int previousToken = Token.FindPrevToken(parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter);
|
||||
|
||||
// See if the number has a preceding "-" separator
|
||||
if (!parser.ParseHelper.IsTokenCategory(previousToken, Token.TokenCategory.Unknown)
|
||||
|| !ParserHelper.IsDashCharacter(parser.Tokens[previousToken].Content[0])) continue;
|
||||
if (!SetEpisodeNumber(parser.Tokens[it].Content, parser.Tokens[it], true)) continue;
|
||||
parser.Tokens[previousToken].Category = Token.TokenCategory.Identifier;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for episode patterns in a list of <code>tokens</code>.
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
/// <returns>true if an episode number was found</returns>
|
||||
public bool SearchForEpisodePatterns(List<int> tokens)
|
||||
{
|
||||
foreach (int it in tokens)
|
||||
{
|
||||
bool numericFront = parser.Tokens[it].Content.Length > 0 && char.IsDigit(parser.Tokens[it].Content[0]);
|
||||
|
||||
if (!numericFront)
|
||||
{
|
||||
// e.g. "EP.1", "Vol.1"
|
||||
if (NumberComesAfterPrefix(ElementCategory.ElementEpisodePrefix, parser.Tokens[it]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (NumberComesAfterPrefix(ElementCategory.ElementVolumePrefix, parser.Tokens[it]))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// e.g. "8 & 10", "01 of 24"
|
||||
if (NumberComesBeforeAnotherNumber(parser.Tokens[it], it))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Look for other patterns
|
||||
if (MatchEpisodePatterns(parser.Tokens[it].Content, parser.Tokens[it]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for equivalent number in a list of <code>tokens</code>. e.g. 08(114)
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
/// <returns>true if an equivalent number was found</returns>
|
||||
public bool SearchForEquivalentNumbers(List<int> tokens)
|
||||
{
|
||||
foreach (int it in tokens)
|
||||
{
|
||||
// Find number must be isolated.
|
||||
if (parser.ParseHelper.IsTokenIsolated(it) || !IsValidEpisodeNumber(parser.Tokens[it].Content))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find the first enclosed, non-delimiter token
|
||||
int nextToken = Token.FindNextToken(parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (!parser.ParseHelper.IsTokenCategory(nextToken, Token.TokenCategory.Bracket)) continue;
|
||||
nextToken = Token.FindNextToken(parser.Tokens, nextToken, Token.TokenFlag.FlagEnclosed,
|
||||
Token.TokenFlag.FlagNotDelimiter);
|
||||
if (!parser.ParseHelper.IsTokenCategory(nextToken, Token.TokenCategory.Unknown)) continue;
|
||||
|
||||
// Check if it's an isolated number
|
||||
if (!parser.ParseHelper.IsTokenIsolated(nextToken)
|
||||
|| !StringHelper.IsNumericString(parser.Tokens[nextToken].Content)
|
||||
|| !IsValidEpisodeNumber(parser.Tokens[nextToken].Content))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var list = new List<Token>
|
||||
{
|
||||
parser.Tokens[it], parser.Tokens[nextToken]
|
||||
};
|
||||
|
||||
list.Sort((o1, o2) => StringHelper.StringToInt(o1.Content) - StringHelper.StringToInt(o2.Content));
|
||||
SetEpisodeNumber(list[0].Content, list[0], false);
|
||||
SetAlternativeEpisodeNumber(list[1].Content, list[1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Searches for the last number token in a list of <code>tokens</code>
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
/// <returns>true if the last number token was found</returns>
|
||||
public bool SearchForLastNumber(List<int> tokens)
|
||||
{
|
||||
for (int i = tokens.Count - 1; i >= 0; i--)
|
||||
{
|
||||
int it = tokens[i];
|
||||
|
||||
// Assuming that episode number always comes after the title,
|
||||
// the first token cannot be what we're looking for
|
||||
if (it == 0) continue;
|
||||
if (parser.Tokens[it].Enclosed) continue;
|
||||
|
||||
// Ignore if it's the first non-enclosed, non-delimiter token
|
||||
if (parser.Tokens.GetRange(0, it)
|
||||
.All(r => r.Enclosed || r.Category == Token.TokenCategory.Delimiter))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int previousToken = Token.FindPrevToken(parser.Tokens, it, Token.TokenFlag.FlagNotDelimiter);
|
||||
if (parser.ParseHelper.IsTokenCategory(previousToken, Token.TokenCategory.Unknown))
|
||||
{
|
||||
if (parser.Tokens[previousToken].Content.Equals("Movie", StringComparison.InvariantCultureIgnoreCase)
|
||||
|| parser.Tokens[previousToken].Content.Equals("Part", StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// We'll use this number after all
|
||||
if (SetEpisodeNumber(parser.Tokens[it].Content, parser.Tokens[it], true))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[GeneratedRegex(@"\A(?:(\d{1,3})[vV](\d))\z")]
|
||||
private static partial Regex SingleEpisodeRegex();
|
||||
|
||||
[GeneratedRegex(@"\A(?:(\d{1,3})(?:[vV](\d))?[-~&+](\d{1,3})(?:[vV](\d))?)\z")]
|
||||
private static partial Regex MultiEpisodeRegex();
|
||||
|
||||
[GeneratedRegex(@"\A(?:S?(\d{1,2})(?:-S?(\d{1,2}))?(?:x|[ ._-x]?E)(\d{1,3})(?:-E?(\d{1,3}))?)\z")]
|
||||
private static partial Regex SeasonEpisodeRegex();
|
||||
|
||||
[GeneratedRegex(@"\A(?:#(\d{1,3})(?:[-~&+](\d{1,3}))?(?:[vV](\d))?)\z")]
|
||||
private static partial Regex NumberEpisodeRegex();
|
||||
|
||||
[GeneratedRegex(@"\A(?:(\d{1,3})話)\z")]
|
||||
private static partial Regex JapaneseEpisodeRegex();
|
||||
|
||||
[GeneratedRegex(@"\A(?:(\d{1,2})[vV](\d))\z")]
|
||||
private static partial Regex SingleVolumeRegex();
|
||||
|
||||
[GeneratedRegex(@"\A(?:(\d{1,2})[-~&+](\d{1,2})(?:[vV](\d))?)\z")]
|
||||
private static partial Regex MultiVolumeRegex();
|
||||
}
|
91
Chiara/AnitomySharp/StringHelper.cs
Normal file
91
Chiara/AnitomySharp/StringHelper.cs
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// A string helper class that is analogous to <code>string.cpp</code> of the original Anitomy, and <code>StringHelper.java</code> of AnitomyJ.
|
||||
/// </summary>
|
||||
public static class StringHelper
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the character is alphanumeric
|
||||
/// </summary>
|
||||
public static bool IsAlphanumericChar(char c)
|
||||
{
|
||||
return c is >= '0' and <= '9' or >= 'A' and <= 'Z' or >= 'a' and <= 'z';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the character is a hex character.
|
||||
/// </summary>
|
||||
private static bool IsHexadecimalChar(char c)
|
||||
{
|
||||
return c is >= '0' and <= '9' or >= 'A' and <= 'F' or >= 'a' and <= 'f';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the character is a latin character
|
||||
/// </summary>
|
||||
private static bool IsLatinChar(char c)
|
||||
{
|
||||
// We're just checking until the end of the Latin Extended-B block,
|
||||
// rather than all the blocks that belong to the Latin script.
|
||||
return c <= '\u024F';
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>str</code> is a hex string.
|
||||
/// </summary>
|
||||
public static bool IsHexadecimalString(string str)
|
||||
{
|
||||
return !string.IsNullOrEmpty(str) && str.All(IsHexadecimalChar);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>str</code> is mostly a latin string.
|
||||
/// </summary>
|
||||
public static bool IsMostlyLatinString(string str)
|
||||
{
|
||||
double length = !string.IsNullOrEmpty(str) ? 1.0 : str.Length;
|
||||
return str.Where(IsLatinChar).Count() / length >= 0.5;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns whether or not the <code>str</code> is a numeric string.
|
||||
/// </summary>
|
||||
public static bool IsNumericString(string str)
|
||||
{
|
||||
return str.All(char.IsDigit);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the int value of the <code>str</code>; 0 otherwise.
|
||||
/// </summary>
|
||||
public static int StringToInt(string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return int.Parse(str);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static string SubstringWithCheck(string str, int start, int count)
|
||||
{
|
||||
if (start + count > str.Length) count = str.Length - start;
|
||||
return str.Substring(start, count);
|
||||
}
|
||||
}
|
226
Chiara/AnitomySharp/Token.cs
Normal file
226
Chiara/AnitomySharp/Token.cs
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// An anime filename is tokenized into individual <see cref="Token"/>s. This class represents an individual token.
|
||||
/// </summary>
|
||||
public class Token
|
||||
{
|
||||
/// <summary>
|
||||
/// The category of the token.
|
||||
/// </summary>
|
||||
public enum TokenCategory
|
||||
{
|
||||
Unknown,
|
||||
Bracket,
|
||||
Delimiter,
|
||||
Identifier,
|
||||
Invalid
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// TokenFlag, used for searching specific token categories. This allows granular searching of TokenCategories.
|
||||
/// </summary>
|
||||
public enum TokenFlag
|
||||
{
|
||||
// None
|
||||
FlagNone,
|
||||
|
||||
// Categories
|
||||
FlagBracket,
|
||||
FlagNotBracket,
|
||||
FlagDelimiter,
|
||||
FlagNotDelimiter,
|
||||
FlagIdentifier,
|
||||
FlagNotIdentifier,
|
||||
FlagUnknown,
|
||||
FlagNotUnknown,
|
||||
FlagValid,
|
||||
FlagNotValid,
|
||||
|
||||
// Enclosed (Meaning that it is enclosed in some bracket (e.g. [ ] ))
|
||||
FlagEnclosed,
|
||||
FlagNotEnclosed
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set of token category flags
|
||||
/// </summary>
|
||||
private static readonly List<TokenFlag> s_flagMaskCategories =
|
||||
[
|
||||
TokenFlag.FlagBracket, TokenFlag.FlagNotBracket,
|
||||
TokenFlag.FlagDelimiter, TokenFlag.FlagNotDelimiter,
|
||||
TokenFlag.FlagIdentifier, TokenFlag.FlagNotIdentifier,
|
||||
TokenFlag.FlagUnknown, TokenFlag.FlagNotUnknown,
|
||||
TokenFlag.FlagValid, TokenFlag.FlagNotValid
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Set of token enclosed flags
|
||||
/// </summary>
|
||||
private static readonly List<TokenFlag> s_flagMaskEnclosed = [TokenFlag.FlagEnclosed, TokenFlag.FlagNotEnclosed];
|
||||
|
||||
public TokenCategory Category { get; set; }
|
||||
public string Content { get; set; }
|
||||
public bool Enclosed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new token
|
||||
/// </summary>
|
||||
/// <param name="category">the token category</param>
|
||||
/// <param name="content">the token content</param>
|
||||
/// <param name="enclosed">whether or not the token is enclosed in braces</param>
|
||||
public Token(TokenCategory category, string content, bool enclosed)
|
||||
{
|
||||
Category = category;
|
||||
Content = content;
|
||||
Enclosed = enclosed;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates a token against the <code>flags</code>. The <code>flags</code> is used as a search parameter.
|
||||
/// </summary>
|
||||
/// <param name="token">the token</param>
|
||||
/// <param name="flags">the flags the token must conform against</param>
|
||||
/// <returns>true if the token conforms to the set of <code>flags</code>; false otherwise</returns>
|
||||
private static bool CheckTokenFlags(Token token, ICollection<TokenFlag> flags)
|
||||
{
|
||||
// Make sure token is the correct closure
|
||||
if (flags.Any(f => s_flagMaskEnclosed.Contains(f)))
|
||||
{
|
||||
bool success = CheckFlag(TokenFlag.FlagEnclosed) == token.Enclosed;
|
||||
if (!success) return false; // Not enclosed correctly (e.g. enclosed when we're looking for non-enclosed).
|
||||
}
|
||||
|
||||
// Make sure token is the correct category
|
||||
if (!flags.Any(f => s_flagMaskCategories.Contains(f))) return true;
|
||||
bool secondarySuccess = false;
|
||||
|
||||
CheckCategory(TokenFlag.FlagBracket, TokenFlag.FlagNotBracket, TokenCategory.Bracket);
|
||||
CheckCategory(TokenFlag.FlagDelimiter, TokenFlag.FlagNotDelimiter, TokenCategory.Delimiter);
|
||||
CheckCategory(TokenFlag.FlagIdentifier, TokenFlag.FlagNotIdentifier, TokenCategory.Identifier);
|
||||
CheckCategory(TokenFlag.FlagUnknown, TokenFlag.FlagNotUnknown, TokenCategory.Unknown);
|
||||
CheckCategory(TokenFlag.FlagNotValid, TokenFlag.FlagValid, TokenCategory.Invalid);
|
||||
return secondarySuccess;
|
||||
|
||||
void CheckCategory(TokenFlag fe, TokenFlag fn, TokenCategory c)
|
||||
{
|
||||
if (secondarySuccess) return;
|
||||
bool result = CheckFlag(fe) ? token.Category == c : CheckFlag(fn) && token.Category != c;
|
||||
secondarySuccess = result;
|
||||
}
|
||||
|
||||
// Simple alias to check if flag is a part of the set
|
||||
bool CheckFlag(TokenFlag flag)
|
||||
{
|
||||
return flags.Contains(flag);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of <code>tokens</code>, searches for any token token that matches the list of <code>flags</code>.
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
/// <param name="begin">the search starting position.</param>
|
||||
/// <param name="end">the search ending position.</param>
|
||||
/// <param name="flags">the search flags</param>
|
||||
/// <returns>the search result</returns>
|
||||
public static int FindToken(List<Token> tokens, int begin, int end, params TokenFlag[] flags)
|
||||
{
|
||||
return FindTokenBase(tokens, begin, end, i => i < tokens.Count, i => i + 1, flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of <code>tokens</code>, searches for the next token in <code>tokens</code> that matches the list of <code>flags</code>.
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
/// <param name="first">the search starting position.</param>
|
||||
/// <param name="flags">the search flags</param>
|
||||
/// <returns>the search result</returns>
|
||||
public static int FindNextToken(List<Token> tokens, int first, params TokenFlag[] flags)
|
||||
{
|
||||
return FindTokenBase(tokens, first + 1, tokens.Count, i => i < tokens.Count, i => i + 1, flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of <code>tokens</code>, searches for the previous token in <code>tokens</code> that matches the list of <code>flags</code>.
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of tokens</param>
|
||||
/// <param name="begin">the search starting position. Exclusive of position.Pos</param>
|
||||
/// <param name="flags">the search flags</param>
|
||||
/// <returns>the search result</returns>
|
||||
public static int FindPrevToken(List<Token> tokens, int begin, params TokenFlag[] flags)
|
||||
{
|
||||
return FindTokenBase(tokens, begin - 1, -1, i => i >= 0, i => i - 1, flags);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Given a list of tokens finds the first token that passes <see cref="CheckTokenFlags"/>.
|
||||
/// </summary>
|
||||
/// <param name="tokens">the list of the tokens to search</param>
|
||||
/// <param name="begin">the start index of the search.</param>
|
||||
/// <param name="end">the end index of the search.</param>
|
||||
/// <param name="shouldContinue">a function that returns whether or not we should continue searching</param>
|
||||
/// <param name="next">a function that returns the next search index</param>
|
||||
/// <param name="flags">the flags that each token should be validated against</param>
|
||||
/// <returns>the found token</returns>
|
||||
private static int FindTokenBase(
|
||||
List<Token> tokens,
|
||||
int begin,
|
||||
int end,
|
||||
Func<int, bool> shouldContinue,
|
||||
Func<int, int> next,
|
||||
params TokenFlag[] flags)
|
||||
{
|
||||
var find = new List<TokenFlag>();
|
||||
find.AddRange(flags);
|
||||
|
||||
for (int i = begin; shouldContinue(i); i = next(i))
|
||||
{
|
||||
var token = tokens[i];
|
||||
if (CheckTokenFlags(token, find))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return end;
|
||||
}
|
||||
|
||||
public static bool InListRange(int pos, List<Token> list)
|
||||
{
|
||||
return -1 < pos && pos < list.Count;
|
||||
}
|
||||
|
||||
public override bool Equals(object? o)
|
||||
{
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o is not Token token)
|
||||
return false;
|
||||
return Enclosed == token.Enclosed && Category == token.Category && Equals(Content, token.Content);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
int hashCode = -1776802967;
|
||||
hashCode = hashCode * -1521134295 + Category.GetHashCode();
|
||||
hashCode = hashCode * -1521134295 + EqualityComparer<string>.Default.GetHashCode(Content);
|
||||
hashCode = hashCode * -1521134295 + Enclosed.GetHashCode();
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Token{{category={Category}, content='{Content}', enclosed={Enclosed}}}";
|
||||
}
|
||||
}
|
17
Chiara/AnitomySharp/TokenRange.cs
Normal file
17
Chiara/AnitomySharp/TokenRange.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
public struct TokenRange(int offset, int size)
|
||||
{
|
||||
public int Offset = offset;
|
||||
public int Size = size;
|
||||
}
|
322
Chiara/AnitomySharp/Tokenizer.cs
Normal file
322
Chiara/AnitomySharp/Tokenizer.cs
Normal file
|
@ -0,0 +1,322 @@
|
|||
/*
|
||||
* Copyright (c) 2014-2017, Eren Okka
|
||||
* Copyright (c) 2016-2017, Paul Miller
|
||||
* Copyright (c) 2017-2018, Tyler Bratton
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
using System.Text;
|
||||
|
||||
namespace AnitomySharp;
|
||||
|
||||
/// <summary>
|
||||
/// A class that will tokenize an anime filename.
|
||||
/// </summary>
|
||||
public class Tokenizer
|
||||
{
|
||||
private readonly string _filename;
|
||||
private readonly List<Element> _elements;
|
||||
private readonly Options _options;
|
||||
private readonly List<Token> _tokens;
|
||||
private static readonly List<Tuple<string, string>> s_brackets =
|
||||
[
|
||||
new Tuple<string, string>("(", ")"), // U+0028-U+0029
|
||||
new Tuple<string, string>("[", "]"), // U+005B-U+005D Square bracket
|
||||
new Tuple<string, string>("{", "}"), // U+007B-U+007D Curly bracket
|
||||
new Tuple<string, string>("\u300C", "\u300D"), // Corner bracket
|
||||
new Tuple<string, string>("\u300E", "\u300E"), // White corner bracket
|
||||
new Tuple<string, string>("\u3010", "\u3011"), // Black lenticular bracket
|
||||
new Tuple<string, string>("\uFF08", "\uFF09")
|
||||
];
|
||||
|
||||
/// <summary>
|
||||
/// Tokenize a filename into <see cref="Element"/>s
|
||||
/// </summary>
|
||||
/// <param name="filename">the filename</param>
|
||||
/// <param name="elements">the list of elements where pre-identified tokens will be added</param>
|
||||
/// <param name="options">the parser options</param>
|
||||
/// <param name="tokens">the list of tokens where tokens will be added</param>
|
||||
public Tokenizer(string filename, List<Element> elements, Options options, List<Token> tokens)
|
||||
{
|
||||
_filename = filename;
|
||||
_elements = elements;
|
||||
_options = options;
|
||||
_tokens = tokens;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true if tokenization was successful; false otherwise.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool Tokenize()
|
||||
{
|
||||
TokenizeByBrackets();
|
||||
return _tokens.Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a token to the internal list of tokens
|
||||
/// </summary>
|
||||
/// <param name="category">the token category</param>
|
||||
/// <param name="enclosed">whether or not the token is enclosed in braces</param>
|
||||
/// <param name="range">the token range</param>
|
||||
private void AddToken(Token.TokenCategory category, bool enclosed, TokenRange range)
|
||||
{
|
||||
_tokens.Add(new Token(category, StringHelper.SubstringWithCheck(_filename, range.Offset, range.Size), enclosed));
|
||||
}
|
||||
|
||||
private string GetDelimiters(TokenRange range)
|
||||
{
|
||||
var delimiters = new StringBuilder();
|
||||
|
||||
foreach (var i in Enumerable.Range(range.Offset, Math.Min(_filename.Length, range.Offset + range.Size) - range.Offset)
|
||||
.Where(value => IsDelimiter(_filename[value])))
|
||||
{
|
||||
delimiters.Append(_filename[i]);
|
||||
}
|
||||
|
||||
return delimiters.ToString();
|
||||
|
||||
bool IsDelimiter(char c)
|
||||
{
|
||||
if (StringHelper.IsAlphanumericChar(c)) return false;
|
||||
return _options.AllowedDelimiters.Contains(c.ToString()) && !delimiters.ToString().Contains(c.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tokenize by bracket.
|
||||
/// </summary>
|
||||
private void TokenizeByBrackets()
|
||||
{
|
||||
string matchingBracket = string.Empty;
|
||||
|
||||
bool isBracketOpen = false;
|
||||
for (int i = 0; i < _filename.Length; )
|
||||
{
|
||||
int foundIdx = !isBracketOpen ? FindFirstBracket(i, _filename.Length) : _filename.IndexOf(matchingBracket, i, StringComparison.Ordinal);
|
||||
|
||||
var range = new TokenRange(i, foundIdx == -1 ? _filename.Length : foundIdx - i);
|
||||
if (range.Size > 0)
|
||||
{
|
||||
// Check if our range contains any known anime identifiers
|
||||
TokenizeByPreIdentified(isBracketOpen, range);
|
||||
}
|
||||
|
||||
if (foundIdx != -1)
|
||||
{
|
||||
// mark as bracket
|
||||
AddToken(Token.TokenCategory.Bracket, true, new TokenRange(range.Offset + range.Size, 1));
|
||||
isBracketOpen = !isBracketOpen;
|
||||
i = foundIdx + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
int FindFirstBracket(int start, int end)
|
||||
{
|
||||
for (int i = start; i < end; i++)
|
||||
{
|
||||
foreach (var bracket in s_brackets)
|
||||
{
|
||||
if (!_filename[i].Equals(char.Parse(bracket.Item1))) continue;
|
||||
matchingBracket = bracket.Item2;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tokenize by looking for known anime identifiers
|
||||
/// </summary>
|
||||
/// <param name="enclosed">whether or not the current <code>range</code> is enclosed in braces</param>
|
||||
/// <param name="range">the token range</param>
|
||||
private void TokenizeByPreIdentified(bool enclosed, TokenRange range)
|
||||
{
|
||||
var preIdentifiedTokens = new List<TokenRange>();
|
||||
|
||||
// Find known anime identifiers
|
||||
KeywordManager.PeekAndAdd(_filename, range, _elements, preIdentifiedTokens);
|
||||
|
||||
int offset = range.Offset;
|
||||
var subRange = new TokenRange(range.Offset, 0);
|
||||
while (offset < range.Offset + range.Size)
|
||||
{
|
||||
foreach (var preIdentifiedToken in preIdentifiedTokens)
|
||||
{
|
||||
if (offset != preIdentifiedToken.Offset) continue;
|
||||
if (subRange.Size > 0)
|
||||
{
|
||||
TokenizeByDelimiters(enclosed, subRange);
|
||||
}
|
||||
|
||||
AddToken(Token.TokenCategory.Identifier, enclosed, preIdentifiedToken);
|
||||
subRange.Offset = preIdentifiedToken.Offset + preIdentifiedToken.Size;
|
||||
offset = subRange.Offset - 1; // It's going to be incremented below
|
||||
}
|
||||
|
||||
subRange.Size = ++offset - subRange.Offset;
|
||||
}
|
||||
|
||||
// Either there was no preidentified token range, or we're now about to process the tail of our current range
|
||||
if (subRange.Size > 0)
|
||||
{
|
||||
TokenizeByDelimiters(enclosed, subRange);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tokenize by delimiters allowed in <see cref="Options"/>.AllowedDelimiters.
|
||||
/// </summary>
|
||||
/// <param name="enclosed">whether or not the current <code>range</code> is enclosed in braces</param>
|
||||
/// <param name="range">the token range</param>
|
||||
private void TokenizeByDelimiters(bool enclosed, TokenRange range)
|
||||
{
|
||||
string delimiters = GetDelimiters(range);
|
||||
|
||||
if (string.IsNullOrEmpty(delimiters))
|
||||
{
|
||||
AddToken(Token.TokenCategory.Unknown, enclosed, range);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = range.Offset, end = range.Offset + range.Size; i < end;)
|
||||
{
|
||||
int found = Enumerable.Range(i, Math.Min(end, _filename.Length) - i)
|
||||
.Where(c => delimiters.Contains(_filename[c].ToString()))
|
||||
.DefaultIfEmpty(end)
|
||||
.FirstOrDefault();
|
||||
|
||||
var subRange = new TokenRange(i, found - i);
|
||||
if (subRange.Size > 0)
|
||||
{
|
||||
AddToken(Token.TokenCategory.Unknown, enclosed, subRange);
|
||||
}
|
||||
|
||||
if (found != end)
|
||||
{
|
||||
AddToken(Token.TokenCategory.Delimiter, enclosed, new TokenRange(subRange.Offset + subRange.Size, 1));
|
||||
i = found + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ValidateDelimiterTokens();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates tokens (make sure certain words delimited by certain tokens aren't split)
|
||||
/// </summary>
|
||||
private void ValidateDelimiterTokens()
|
||||
{
|
||||
for (int i = 0; i < _tokens.Count; i++)
|
||||
{
|
||||
var token = _tokens[i];
|
||||
if (token.Category != Token.TokenCategory.Delimiter) continue;
|
||||
char delimiter = token.Content[0];
|
||||
|
||||
int prevToken = Token.FindPrevToken(_tokens, i, Token.TokenFlag.FlagValid);
|
||||
int nextToken = Token.FindNextToken(_tokens, i, Token.TokenFlag.FlagValid);
|
||||
|
||||
// Check for single-character tokens to prevent splitting group names,
|
||||
// keywords, episode numbers, etc.
|
||||
if (delimiter != ' ' && delimiter != '_')
|
||||
{
|
||||
|
||||
// Single character token
|
||||
if (IsSingleCharacterToken(prevToken))
|
||||
{
|
||||
AppendTokenTo(token, _tokens[prevToken]);
|
||||
|
||||
while (IsUnknownToken(nextToken))
|
||||
{
|
||||
AppendTokenTo(_tokens[nextToken], _tokens[prevToken]);
|
||||
|
||||
nextToken = Token.FindNextToken(_tokens, i, Token.TokenFlag.FlagValid);
|
||||
if (!IsDelimiterToken(nextToken) || _tokens[nextToken].Content[0] != delimiter) continue;
|
||||
AppendTokenTo(_tokens[nextToken], _tokens[prevToken]);
|
||||
nextToken = Token.FindNextToken(_tokens, nextToken, Token.TokenFlag.FlagValid);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsSingleCharacterToken(nextToken))
|
||||
{
|
||||
AppendTokenTo(token, _tokens[prevToken]);
|
||||
AppendTokenTo(_tokens[nextToken], _tokens[prevToken]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for adjacent delimiters
|
||||
if (IsUnknownToken(prevToken) && IsDelimiterToken(nextToken))
|
||||
{
|
||||
char nextDelimiter = _tokens[nextToken].Content[0];
|
||||
if (delimiter != nextDelimiter && delimiter != ',')
|
||||
{
|
||||
if (nextDelimiter is ' ' or '_')
|
||||
{
|
||||
AppendTokenTo(token, _tokens[prevToken]);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (IsDelimiterToken(prevToken) && IsDelimiterToken(nextToken))
|
||||
{
|
||||
char prevDelimiter = _tokens[prevToken].Content[0];
|
||||
char nextDelimiter = _tokens[nextToken].Content[0];
|
||||
if (prevDelimiter == nextDelimiter && prevDelimiter != delimiter)
|
||||
{
|
||||
token.Category = Token.TokenCategory.Unknown; // e.g. "& in "_&_"
|
||||
}
|
||||
}
|
||||
|
||||
// Check for other special cases
|
||||
if (delimiter != '&' && delimiter != '+') continue;
|
||||
if (!IsUnknownToken(prevToken) || !IsUnknownToken(nextToken)) continue;
|
||||
if (!StringHelper.IsNumericString(_tokens[prevToken].Content)
|
||||
|| !StringHelper.IsNumericString(_tokens[nextToken].Content)) continue;
|
||||
AppendTokenTo(token, _tokens[prevToken]);
|
||||
AppendTokenTo(_tokens[nextToken], _tokens[prevToken]); // e.g. 01+02
|
||||
}
|
||||
|
||||
// Remove invalid tokens
|
||||
_tokens.RemoveAll(token => token.Category == Token.TokenCategory.Invalid);
|
||||
return;
|
||||
|
||||
void AppendTokenTo(Token src, Token dest)
|
||||
{
|
||||
dest.Content += src.Content;
|
||||
src.Category = Token.TokenCategory.Invalid;
|
||||
}
|
||||
|
||||
bool IsUnknownToken(int it)
|
||||
{
|
||||
return Token.InListRange(it, _tokens) && _tokens[it].Category == Token.TokenCategory.Unknown;
|
||||
}
|
||||
|
||||
bool IsSingleCharacterToken(int it)
|
||||
{
|
||||
return IsUnknownToken(it) && _tokens[it].Content.Length == 1 && _tokens[it].Content[0] != '-';
|
||||
}
|
||||
|
||||
bool IsDelimiterToken(int it)
|
||||
{
|
||||
return Token.InListRange(it, _tokens) && _tokens[it].Category == Token.TokenCategory.Delimiter;
|
||||
}
|
||||
}
|
||||
}
|
3
Chiara/Chiara.Tests/.gitignore
vendored
Normal file
3
Chiara/Chiara.Tests/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
Data/
|
||||
*.m3u8
|
||||
*.ts
|
91
Chiara/Chiara.Tests/AnitomySharpTests/DataTests.cs
Normal file
91
Chiara/Chiara.Tests/AnitomySharpTests/DataTests.cs
Normal file
|
@ -0,0 +1,91 @@
|
|||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using AnitomySharp;
|
||||
|
||||
namespace Chiara.Tests.AnitomySharpTests;
|
||||
|
||||
public class TestCase
|
||||
{
|
||||
[JsonPropertyName("file_name")]
|
||||
public string FileName { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("ignore")]
|
||||
public bool Ignore { get; init; }
|
||||
|
||||
[JsonPropertyName("results")]
|
||||
public Dictionary<string, JsonElement> Results { get; init; } = [];
|
||||
}
|
||||
|
||||
public class DataTests(ITestOutputHelper helper)
|
||||
{
|
||||
[Fact]
|
||||
public void ValidateParsingResults()
|
||||
{
|
||||
string path = Path.Combine(Environment.CurrentDirectory, "test-cases.json");
|
||||
List<TestCase>? cases = JsonSerializer.Deserialize<List<TestCase>>(File.ReadAllText(path));
|
||||
Assert.NotNull(cases);
|
||||
|
||||
helper.WriteLine($"Loaded {cases.Count} test cases.");
|
||||
foreach (var testCase in cases)
|
||||
{
|
||||
Verify(testCase);
|
||||
}
|
||||
}
|
||||
|
||||
private void Verify(TestCase entry)
|
||||
{
|
||||
string fileName = entry.FileName;
|
||||
bool ignore = entry.Ignore;
|
||||
Dictionary<string, JsonElement> testCases = entry.Results;
|
||||
|
||||
if (ignore || string.IsNullOrWhiteSpace(fileName) || testCases.Count == 0)
|
||||
{
|
||||
helper.WriteLine($"Ignoring [{fileName}] : {{ results: {testCases.Count} | explicit: {ignore}}}");
|
||||
return;
|
||||
}
|
||||
|
||||
helper.WriteLine($"Parsing: {fileName}");
|
||||
Dictionary<string, List<string>> parseResults = ToTestCaseDict(fileName);
|
||||
|
||||
foreach (KeyValuePair<string,JsonElement> pair in testCases)
|
||||
{
|
||||
Assert.True(parseResults.TryGetValue(pair.Key, out List<string>? value));
|
||||
Assert.NotNull(value);
|
||||
|
||||
if (pair.Value.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
Assert.Contains(pair.Value.GetString(), value);
|
||||
}
|
||||
|
||||
if (pair.Value.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
List<string>? exceptedArray = pair.Value.Deserialize<List<string>>();
|
||||
Assert.NotNull(exceptedArray);
|
||||
|
||||
Assert.Equal(exceptedArray, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<string>> ToTestCaseDict(string filename)
|
||||
{
|
||||
var parseResults = Anitomy.Parse(filename);
|
||||
var elements = new Dictionary<string, List<string>>();
|
||||
|
||||
foreach (var e in parseResults)
|
||||
{
|
||||
if (elements.TryGetValue(e.Category.ToString(), out List<string>? value))
|
||||
{
|
||||
value.Add(e.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
elements.Add(e.Category.ToString(), [e.Value]);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
}
|
2380
Chiara/Chiara.Tests/AnitomySharpTests/test-cases.json
Normal file
2380
Chiara/Chiara.Tests/AnitomySharpTests/test-cases.json
Normal file
File diff suppressed because it is too large
Load Diff
34
Chiara/Chiara.Tests/Chiara.Tests.csproj
Normal file
34
Chiara/Chiara.Tests/Chiara.Tests.csproj
Normal file
|
@ -0,0 +1,34 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0"/>
|
||||
<PackageReference Include="Moq" Version="4.20.70"/>
|
||||
<PackageReference Include="xunit" Version="2.4.2"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Chiara\Chiara.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
<Target Name="CopyTestFiles" AfterTargets="Build">
|
||||
<Copy SourceFiles="AnitomySharpTests/test-cases.json" DestinationFolder="$(OutputPath)" SkipUnchangedFiles="true"/>
|
||||
</Target>
|
||||
|
||||
</Project>
|
3
Chiara/Chiara.Tests/GlobalUsings.cs
Normal file
3
Chiara/Chiara.Tests/GlobalUsings.cs
Normal file
|
@ -0,0 +1,3 @@
|
|||
global using Xunit;
|
||||
global using Xunit.Abstractions;
|
||||
global using Moq;
|
27
Chiara/Chiara.Tests/Services/MediaRepositoryScannerTests.cs
Normal file
27
Chiara/Chiara.Tests/Services/MediaRepositoryScannerTests.cs
Normal file
|
@ -0,0 +1,27 @@
|
|||
using Chiara.Abstractions;
|
||||
using Chiara.Models;
|
||||
using Chiara.Services;
|
||||
using Chiara.Tests.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Chiara.Tests.Services;
|
||||
|
||||
public class MediaRepositoryScannerTests
|
||||
{
|
||||
private readonly DefaultMediaRepositoryScanner _scanner = new(
|
||||
MockCreator.CreateEmptyMock<IFileStore>(),
|
||||
MockCreator.CreateEmptyMock<ILogger<DefaultMediaRepositoryScanner>>());
|
||||
|
||||
[Fact]
|
||||
public async Task ScanTest()
|
||||
{
|
||||
MediaRepository repository = new() { Path = "/home/ricardo/Documents/Code/CSharp/Chiara/Chiara.Tests/Data" };
|
||||
|
||||
List<Album> albums = (await _scanner.ScanAlbumAsync(repository)).ToList();
|
||||
|
||||
Assert.Single(albums);
|
||||
Assert.Contains(albums, a => a.Title == "Genshin Impact - Jade Moon Upon a Sea of Clouds");
|
||||
Assert.Contains(albums, a => a.Arist == "Yu-Peng Chen, HOYO-MiX");
|
||||
Assert.Contains(albums, a => a.Songs.Count == 69);
|
||||
}
|
||||
}
|
20
Chiara/Chiara.Tests/Utils/MockCreator.cs
Normal file
20
Chiara/Chiara.Tests/Utils/MockCreator.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using Chiara.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Chiara.Tests.Utils;
|
||||
|
||||
public static class MockCreator
|
||||
{
|
||||
public static ILogger<T> CreateNoOutputLogger<T>()
|
||||
{
|
||||
Mock<ILogger<T>> mock = new();
|
||||
|
||||
return mock.Object;
|
||||
}
|
||||
|
||||
public static T CreateEmptyMock<T>() where T : class
|
||||
{
|
||||
Mock<T> mock = new();
|
||||
return mock.Object;
|
||||
}
|
||||
}
|
47
Chiara/Chiara.sln
Normal file
47
Chiara/Chiara.sln
Normal file
|
@ -0,0 +1,47 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chiara", "Chiara\Chiara.csproj", "{06FB2F9E-6C0A-4D73-A1B0-D60B116348FB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Chiara.Tests", "Chiara.Tests\Chiara.Tests.csproj", "{DD3DCD44-D661-4DE1-B2FD-170D5504361A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utils", "Utils", "{E6D11D0F-E915-41B5-86D4-CCEA8C6CF3A3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AnitomySharp", "AnitomySharp\AnitomySharp.csproj", "{4893AB7B-D50D-4C06-9267-E14F28B510E9}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".gitea", ".gitea", "{38E1A429-AC02-4C7A-AF97-338D9FF30A0B}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{DB9235C8-F148-47B8-A205-BCF978750D73}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.gitea\workflows\build.yaml = .gitea\workflows\build.yaml
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{06FB2F9E-6C0A-4D73-A1B0-D60B116348FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{06FB2F9E-6C0A-4D73-A1B0-D60B116348FB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{06FB2F9E-6C0A-4D73-A1B0-D60B116348FB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{06FB2F9E-6C0A-4D73-A1B0-D60B116348FB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{DD3DCD44-D661-4DE1-B2FD-170D5504361A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{DD3DCD44-D661-4DE1-B2FD-170D5504361A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{DD3DCD44-D661-4DE1-B2FD-170D5504361A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{DD3DCD44-D661-4DE1-B2FD-170D5504361A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{4893AB7B-D50D-4C06-9267-E14F28B510E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{4893AB7B-D50D-4C06-9267-E14F28B510E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{4893AB7B-D50D-4C06-9267-E14F28B510E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{4893AB7B-D50D-4C06-9267-E14F28B510E9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{4893AB7B-D50D-4C06-9267-E14F28B510E9} = {E6D11D0F-E915-41B5-86D4-CCEA8C6CF3A3}
|
||||
{DB9235C8-F148-47B8-A205-BCF978750D73} = {38E1A429-AC02-4C7A-AF97-338D9FF30A0B}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
10
Chiara/Chiara/Abstractions/IFile.cs
Normal file
10
Chiara/Chiara/Abstractions/IFile.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Chiara.Abstractions;
|
||||
|
||||
public interface IFile
|
||||
{
|
||||
public Guid FileId { get; }
|
||||
|
||||
public string ContentType { get; }
|
||||
|
||||
public Stream OpenRead();
|
||||
}
|
12
Chiara/Chiara/Abstractions/IFileStore.cs
Normal file
12
Chiara/Chiara/Abstractions/IFileStore.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.Abstractions;
|
||||
|
||||
public interface IFileStore
|
||||
{
|
||||
public Task<string> UploadFileAsync(ReadOnlyMemory<byte> buffer, string contentType);
|
||||
|
||||
public Task<string> ClarifyLocalFileAsync(string path, string contentType);
|
||||
|
||||
public Task<IFile?> GetFileAsync(Guid guid);
|
||||
}
|
10
Chiara/Chiara/Abstractions/IMediaItem.cs
Normal file
10
Chiara/Chiara/Abstractions/IMediaItem.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Chiara.Abstractions;
|
||||
|
||||
public interface IMediaItem
|
||||
{
|
||||
public string Title { get; }
|
||||
|
||||
public string Arist { get; }
|
||||
|
||||
public string Path { get; }
|
||||
}
|
12
Chiara/Chiara/Abstractions/IMediaRepositoryScanner.cs
Normal file
12
Chiara/Chiara/Abstractions/IMediaRepositoryScanner.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.Abstractions;
|
||||
|
||||
public interface IMediaRepositoryScanner
|
||||
{
|
||||
public Task<IEnumerable<Album>> ScanAlbumAsync(MediaRepository repository,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
public Task<IEnumerable<ShowSeason>> ScanShowAsync(MediaRepository repository,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
34
Chiara/Chiara/Chiara.csproj
Normal file
34
Chiara/Chiara/Chiara.csproj
Normal file
|
@ -0,0 +1,34 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
|
||||
<PackageReference Include="TagLibSharp" Version="2.3.0" />
|
||||
<PackageReference Include="Xabe.FFmpeg" Version="5.2.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="../.editorconfig" />
|
||||
<None Include="../.gitignore" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="wwwroot\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\AnitomySharp\AnitomySharp.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
40
Chiara/Chiara/Controllers/AlbumController.cs
Normal file
40
Chiara/Chiara/Controllers/AlbumController.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Chiara.DataTransferObjects;
|
||||
using Chiara.Models;
|
||||
using Chiara.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Chiara.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class AlbumController(ChiaraDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<AlbumResponse>>(200)]
|
||||
public async Task<IActionResult> GetAllAlbums()
|
||||
{
|
||||
IQueryable<AlbumResponse> albumResponses = from item in dbContext.Albums.AsNoTracking()
|
||||
select new AlbumResponse(item);
|
||||
|
||||
return Ok(await albumResponses.ToListAsync());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType<AlbumResponse>(200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> GetAlbum([FromRoute] int id)
|
||||
{
|
||||
Album? album = await (from item in dbContext.Albums.AsNoTracking()
|
||||
.Include(a => a.Songs)
|
||||
where item.Id == id
|
||||
select item).FirstOrDefaultAsync();
|
||||
|
||||
if (album is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(new AlbumResponse(album));
|
||||
}
|
||||
}
|
28
Chiara/Chiara/Controllers/FileController.cs
Normal file
28
Chiara/Chiara/Controllers/FileController.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using Chiara.Abstractions;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Chiara.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class FileController(IFileStore fileStore) : ControllerBase
|
||||
{
|
||||
[HttpGet("{key}")]
|
||||
[ProducesResponseType<FileStreamResult>(200)]
|
||||
public async Task<IActionResult> DownloadFile([FromRoute] string key)
|
||||
{
|
||||
if (!Guid.TryParse(key, out Guid guid))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
IFile? file = await fileStore.GetFileAsync(guid);
|
||||
|
||||
if (file is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return File(file.OpenRead(), file.ContentType);
|
||||
}
|
||||
}
|
152
Chiara/Chiara/Controllers/HlsController.cs
Normal file
152
Chiara/Chiara/Controllers/HlsController.cs
Normal file
|
@ -0,0 +1,152 @@
|
|||
using System.Net.Mime;
|
||||
using Chiara.DataTransferObjects;
|
||||
using Chiara.Models;
|
||||
using Chiara.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Chiara.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class HlsController(
|
||||
FfmpegService ffmpegService,
|
||||
ChiaraDbContext dbContext,
|
||||
ILogger<HlsController> logger,
|
||||
IOptions<ChiaraOptions> chiaraOptions) : ControllerBase
|
||||
{
|
||||
private readonly DirectoryInfo _cacheDirectory = new(chiaraOptions.Value.TemporaryDirectory);
|
||||
|
||||
[HttpGet("{file}")]
|
||||
public IActionResult File([FromRoute] string file)
|
||||
{
|
||||
FileInfo fileInfo = new(Path.Combine(_cacheDirectory.FullName, file));
|
||||
|
||||
if (!fileInfo.Exists)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (file.EndsWith("m3u8"))
|
||||
{
|
||||
return File(fileInfo.OpenRead(), "application/x-mpegURL");
|
||||
}
|
||||
|
||||
if (file.EndsWith("ts"))
|
||||
{
|
||||
return File(fileInfo.OpenRead(), "video/MP2T");
|
||||
}
|
||||
|
||||
return File(fileInfo.OpenRead(), MediaTypeNames.Text.Plain);
|
||||
}
|
||||
|
||||
[HttpPost("start/{episodeId}")]
|
||||
[ProducesResponseType<VideoPlayingResponse>(200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> Start([FromRoute] int episodeId)
|
||||
{
|
||||
Episode? episode = await (from item in dbContext.Episodes.AsNoTracking()
|
||||
where item.Id == episodeId
|
||||
select item).FirstOrDefaultAsync();
|
||||
|
||||
if (episode is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (ffmpegService.CurrentConversion is not null)
|
||||
{
|
||||
logger.LogWarning("Stop running transaction rudely.");
|
||||
try
|
||||
{
|
||||
await ffmpegService.CurrentConversion.RunningTask;
|
||||
ffmpegService.CurrentConversion = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogInformation("Stop transaction encounter: {}.", e);
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogInformation("Start transaction of {}.", episode.Title);
|
||||
|
||||
FileInfo videoFile = new(episode.Path);
|
||||
string name = Guid.NewGuid().ToString();
|
||||
CancellationTokenSource source = new();
|
||||
|
||||
VideoConversion conversion = new(
|
||||
ffmpegService.StartConversion(videoFile, _cacheDirectory, name, source.Token),
|
||||
_cacheDirectory,
|
||||
name,
|
||||
source);
|
||||
|
||||
ffmpegService.CurrentConversion = conversion;
|
||||
|
||||
using PeriodicTimer time = new(TimeSpan.FromSeconds(1));
|
||||
|
||||
while (!_cacheDirectory.EnumerateFiles().Any(f => f.Name.StartsWith(name)))
|
||||
{
|
||||
await time.WaitForNextTickAsync();
|
||||
}
|
||||
|
||||
return Ok(new VideoPlayingResponse { PlayList = $"/api/hls/{name}.m3u8" });
|
||||
}
|
||||
|
||||
[HttpPost("stop")]
|
||||
[ProducesResponseType(200)]
|
||||
public async Task<IActionResult> Stop()
|
||||
{
|
||||
if (ffmpegService.CurrentConversion is null || ffmpegService.CurrentConversion.RunningTask.IsCanceled)
|
||||
{
|
||||
if (ffmpegService.CurrentConversion is not null)
|
||||
{
|
||||
logger.LogInformation("Stop finished video transaction.");
|
||||
try
|
||||
{
|
||||
await ffmpegService.CurrentConversion.RunningTask;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogInformation("Video encounter: {}.", e);
|
||||
}
|
||||
|
||||
ffmpegService.CurrentConversion = null;
|
||||
}
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
logger.LogInformation("Stop running video transaction.");
|
||||
await ffmpegService.CurrentConversion.CancellationTokenSource.CancelAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await ffmpegService.CurrentConversion.RunningTask;
|
||||
ffmpegService.CurrentConversion = null;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogInformation("Video encounter: {}.", e);
|
||||
}
|
||||
|
||||
ClearCacheDirectory();
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private void ClearCacheDirectory()
|
||||
{
|
||||
IEnumerable<FileInfo> temporaryFiles = from file in _cacheDirectory.EnumerateFiles()
|
||||
where file.Extension is ".m3u8" or ".ts"
|
||||
select file;
|
||||
|
||||
foreach (FileInfo file in temporaryFiles)
|
||||
{
|
||||
if (file.Exists)
|
||||
{
|
||||
file.Delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
74
Chiara/Chiara/Controllers/MediaRepositoryController.cs
Normal file
74
Chiara/Chiara/Controllers/MediaRepositoryController.cs
Normal file
|
@ -0,0 +1,74 @@
|
|||
using Chiara.DataTransferObjects;
|
||||
using Chiara.Models;
|
||||
using Chiara.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Chiara.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class MediaRepositoryController(RefreshService refreshService, ChiaraDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<MediaRepositoryResponse>>(200)]
|
||||
public async Task<IActionResult> ListRepositories()
|
||||
{
|
||||
return Ok(await (from item in dbContext.Repositories.AsNoTracking()
|
||||
select new MediaRepositoryResponse(item)).ToListAsync());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType<MediaRepositoryResponse>(200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> GetRepository([FromRoute] int id)
|
||||
{
|
||||
MediaRepository? repository = await (from item in dbContext.Repositories.AsNoTracking()
|
||||
.Include(m => m.Albums)
|
||||
.Include(m => m.Seasons)
|
||||
where item.Id == id
|
||||
select item).FirstOrDefaultAsync();
|
||||
|
||||
if (repository is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(new MediaRepositoryResponse(repository));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
[ProducesResponseType<MediaRepositoryResponse>(201)]
|
||||
public async Task<IActionResult> CreateRepository([FromBody] CreateMediaRepositoryRequest request)
|
||||
{
|
||||
MediaRepository repository = new() { Name = request.Name, Path = request.Path };
|
||||
|
||||
await dbContext.Repositories.AddAsync(repository);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
return Created($"api/MediaRepository/{repository.Id}", new MediaRepositoryResponse(repository));
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[ProducesResponseType<MediaRepositoryResponse>(200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> RefreshRepository([FromRoute] int id)
|
||||
{
|
||||
IQueryable<MediaRepository> mediaRepositoryQuery = from item in dbContext.Repositories
|
||||
.Include(m => m.Albums)
|
||||
.Include(m => m.Seasons)
|
||||
where item.Id == id
|
||||
select item;
|
||||
|
||||
MediaRepository? repository = await mediaRepositoryQuery.FirstOrDefaultAsync();
|
||||
|
||||
if (repository is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
await refreshService.Refresh(repository);
|
||||
|
||||
return Ok(new MediaRepositoryResponse(await mediaRepositoryQuery.AsNoTracking().FirstAsync()));
|
||||
}
|
||||
}
|
38
Chiara/Chiara/Controllers/SeasonController.cs
Normal file
38
Chiara/Chiara/Controllers/SeasonController.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Chiara.DataTransferObjects;
|
||||
using Chiara.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Chiara.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("api/[controller]")]
|
||||
public class SeasonController(ChiaraDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<ShowSeasonResponse>>(200)]
|
||||
public async Task<IActionResult> ListSeasons()
|
||||
{
|
||||
return Ok(await (from item in dbContext.Seasons.AsNoTracking()
|
||||
select new ShowSeasonResponse(item)).ToListAsync());
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType<ShowSeasonResponse>(200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> GetSeason([FromRoute] int id)
|
||||
{
|
||||
ShowSeasonResponse? response = await (from item in dbContext.Seasons.AsNoTracking()
|
||||
.Include(s => s.Episodes
|
||||
.OrderBy(e => e.EpisodeNumber))
|
||||
where item.Id == id
|
||||
select new ShowSeasonResponse(item)).FirstOrDefaultAsync();
|
||||
|
||||
if (response is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
}
|
39
Chiara/Chiara/Controllers/SongController.cs
Normal file
39
Chiara/Chiara/Controllers/SongController.cs
Normal file
|
@ -0,0 +1,39 @@
|
|||
using Chiara.DataTransferObjects;
|
||||
using Chiara.Models;
|
||||
using Chiara.Services;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Chiara.Controllers;
|
||||
|
||||
[Route("api/[controller]")]
|
||||
[ApiController]
|
||||
public class SongController(ChiaraDbContext dbContext) : ControllerBase
|
||||
{
|
||||
[HttpGet]
|
||||
[ProducesResponseType<IEnumerable<SongResponse>>(200)]
|
||||
public IActionResult ListSongs()
|
||||
{
|
||||
IEnumerable<SongResponse> response = from item in dbContext.Songs.AsNoTracking()
|
||||
select new SongResponse(item);
|
||||
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
[ProducesResponseType<SongResponse>(200)]
|
||||
[ProducesResponseType(404)]
|
||||
public async Task<IActionResult> GetSong([FromRoute] int id)
|
||||
{
|
||||
Song? song = await (from item in dbContext.Songs.AsNoTracking()
|
||||
where item.Id == id
|
||||
select item).FirstOrDefaultAsync();
|
||||
|
||||
if (song is null)
|
||||
{
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return Ok(new SongResponse(song));
|
||||
}
|
||||
}
|
36
Chiara/Chiara/DataTransferObjects/AlbumResponse.cs
Normal file
36
Chiara/Chiara/DataTransferObjects/AlbumResponse.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.DataTransferObjects;
|
||||
|
||||
public class AlbumResponse
|
||||
{
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Arist { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string CoverImageUrl { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public List<SongResponse> Songs { get; set; } = [];
|
||||
|
||||
public AlbumResponse()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public AlbumResponse(Album album)
|
||||
{
|
||||
Id = album.Id;
|
||||
Title = album.Title;
|
||||
Arist = album.Arist;
|
||||
CoverImageUrl = album.CoverImageUrl;
|
||||
Songs = album.Songs.Select(s => new SongResponse(s)).ToList();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Chiara.DataTransferObjects;
|
||||
|
||||
public class CreateMediaRepositoryRequest
|
||||
{
|
||||
[Required]
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
}
|
32
Chiara/Chiara/DataTransferObjects/EpisodeResponse.cs
Normal file
32
Chiara/Chiara/DataTransferObjects/EpisodeResponse.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.DataTransferObjects;
|
||||
|
||||
public class EpisodeResponse
|
||||
{
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string EpisodeNumber { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public int ShowSeasonId { get; set; }
|
||||
|
||||
public EpisodeResponse()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public EpisodeResponse(Episode episode)
|
||||
{
|
||||
Id = episode.Id;
|
||||
Title = episode.Title;
|
||||
EpisodeNumber = episode.EpisodeNumber;
|
||||
ShowSeasonId = episode.ShowSeasonId;
|
||||
}
|
||||
}
|
30
Chiara/Chiara/DataTransferObjects/MediaRepositoryResponse.cs
Normal file
30
Chiara/Chiara/DataTransferObjects/MediaRepositoryResponse.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.DataTransferObjects;
|
||||
|
||||
public class MediaRepositoryResponse
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public List<AlbumResponse> AlbumResponses { get; set; } = [];
|
||||
|
||||
public List<ShowSeasonResponse> ShowSeasonResponses { get; set; } = [];
|
||||
|
||||
public MediaRepositoryResponse()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public MediaRepositoryResponse(MediaRepository repository)
|
||||
{
|
||||
Id = repository.Id;
|
||||
Path = repository.Path;
|
||||
Name = repository.Name;
|
||||
AlbumResponses.AddRange(repository.Albums.Select(a => new AlbumResponse(a)));
|
||||
ShowSeasonResponses.AddRange(repository.Seasons.Select(s => new ShowSeasonResponse(s)));
|
||||
}
|
||||
}
|
28
Chiara/Chiara/DataTransferObjects/ShowSeasonResponse.cs
Normal file
28
Chiara/Chiara/DataTransferObjects/ShowSeasonResponse.cs
Normal file
|
@ -0,0 +1,28 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.DataTransferObjects;
|
||||
|
||||
public class ShowSeasonResponse
|
||||
{
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public List<EpisodeResponse> Episodes { get; set; } = [];
|
||||
|
||||
public ShowSeasonResponse()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public ShowSeasonResponse(ShowSeason showSeason)
|
||||
{
|
||||
Id = showSeason.Id;
|
||||
Title = showSeason.Title;
|
||||
Episodes.AddRange(showSeason.Episodes.Select(e => new EpisodeResponse(e)));
|
||||
}
|
||||
}
|
36
Chiara/Chiara/DataTransferObjects/SongResponse.cs
Normal file
36
Chiara/Chiara/DataTransferObjects/SongResponse.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.DataTransferObjects;
|
||||
|
||||
public class SongResponse
|
||||
{
|
||||
[Required]
|
||||
public int Id { get; set; }
|
||||
|
||||
[Required]
|
||||
public string Title { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Arist { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string CoverImageUrl { get; set; } = string.Empty;
|
||||
|
||||
[Required]
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
public SongResponse()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public SongResponse(Song song)
|
||||
{
|
||||
Id = song.Id;
|
||||
Title = song.Title;
|
||||
Arist = song.Arist;
|
||||
CoverImageUrl = song.CoverImageUrl;
|
||||
Url = song.Url;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Chiara.DataTransferObjects;
|
||||
|
||||
public class VideoPlayingResponse
|
||||
{
|
||||
[Required]
|
||||
public string PlayList { get; set; } = string.Empty;
|
||||
}
|
8
Chiara/Chiara/Dockerfile
Normal file
8
Chiara/Chiara/Dockerfile
Normal file
|
@ -0,0 +1,8 @@
|
|||
FROM mcr.microsoft.com/dotnet/aspnet:8.0
|
||||
|
||||
RUN sed -i "s?http://deb.debian.org?https://mirrors.cernet.edu.cn?g" /etc/apt/sources.list.d/debian.sources
|
||||
RUN apt update && apt upgrade -y && apt install -y ffmpeg
|
||||
|
||||
WORKDIR /App
|
||||
COPY bin/Release/net8.0/publish /App
|
||||
ENTRYPOINT ["dotnet", "/App/Chiara.dll"]
|
19
Chiara/Chiara/Extensions/ServiceCollectionExtensions.cs
Normal file
19
Chiara/Chiara/Extensions/ServiceCollectionExtensions.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using Chiara.Abstractions;
|
||||
using Chiara.Services;
|
||||
|
||||
namespace Chiara.Extensions;
|
||||
|
||||
public static class ServiceCollectionExtensions
|
||||
{
|
||||
public static void AddChiaraService(this IServiceCollection serviceCollection)
|
||||
{
|
||||
serviceCollection.AddSingleton<LocalFileService>();
|
||||
serviceCollection.AddSingleton<FileService>();
|
||||
serviceCollection.AddSingleton<IFileStore, FileService>(provider => provider.GetRequiredService<FileService>());
|
||||
serviceCollection.AddTransient<IMediaRepositoryScanner, DefaultMediaRepositoryScanner>();
|
||||
serviceCollection.AddTransient<RefreshService>();
|
||||
serviceCollection.AddHostedService<MigrationService>();
|
||||
serviceCollection.AddHostedService<FileService>(provider => provider.GetRequiredService<FileService>());
|
||||
serviceCollection.AddSingleton<FfmpegService>();
|
||||
}
|
||||
}
|
12
Chiara/Chiara/Extensions/WebApplicationBuilderExtensions.cs
Normal file
12
Chiara/Chiara/Extensions/WebApplicationBuilderExtensions.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.Extensions;
|
||||
|
||||
public static class WebApplicationBuilderExtensions
|
||||
{
|
||||
public static WebApplicationBuilder AddChiaraOptions(this WebApplicationBuilder builder)
|
||||
{
|
||||
builder.Services.Configure<ChiaraOptions>(builder.Configuration.GetSection(ChiaraOptions.OptionName));
|
||||
return builder;
|
||||
}
|
||||
}
|
58
Chiara/Chiara/Migrations/20240521123215_CreateDatabase.Designer.cs
generated
Normal file
58
Chiara/Chiara/Migrations/20240521123215_CreateDatabase.Designer.cs
generated
Normal file
|
@ -0,0 +1,58 @@
|
|||
// <auto-generated />
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240521123215_CreateDatabase")]
|
||||
partial class CreateDatabase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.5")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Author")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
38
Chiara/Chiara/Migrations/20240521123215_CreateDatabase.cs
Normal file
38
Chiara/Chiara/Migrations/20240521123215_CreateDatabase.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class CreateDatabase : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Songs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Author = table.Column<string>(type: "text", nullable: false),
|
||||
Url = table.Column<string>(type: "text", nullable: false),
|
||||
CoverImageUrl = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Songs", x => x.Id);
|
||||
});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Songs");
|
||||
}
|
||||
}
|
||||
}
|
131
Chiara/Chiara/Migrations/20240529140720_AddAlbum.Designer.cs
generated
Normal file
131
Chiara/Chiara/Migrations/20240529140720_AddAlbum.Designer.cs
generated
Normal file
|
@ -0,0 +1,131 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240529140720_AddAlbum")]
|
||||
partial class AddAlbum
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("ForeignId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ForeignId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
113
Chiara/Chiara/Migrations/20240529140720_AddAlbum.cs
Normal file
113
Chiara/Chiara/Migrations/20240529140720_AddAlbum.cs
Normal file
|
@ -0,0 +1,113 @@
|
|||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddAlbum : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Url",
|
||||
table: "Songs",
|
||||
newName: "Path");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Author",
|
||||
table: "Songs",
|
||||
newName: "Arist");
|
||||
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "AlbumId",
|
||||
table: "Songs",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Albums",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Arist = table.Column<string>(type: "text", nullable: false),
|
||||
Path = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Albums", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Files",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
ForeignId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Content = table.Column<byte[]>(type: "bytea", nullable: false),
|
||||
ContentType = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Files", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Songs_AlbumId",
|
||||
table: "Songs",
|
||||
column: "AlbumId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Files_ForeignId",
|
||||
table: "Files",
|
||||
column: "ForeignId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Songs_Albums_AlbumId",
|
||||
table: "Songs",
|
||||
column: "AlbumId",
|
||||
principalTable: "Albums",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Songs_Albums_AlbumId",
|
||||
table: "Songs");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Albums");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Files");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Songs_AlbumId",
|
||||
table: "Songs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "AlbumId",
|
||||
table: "Songs");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Path",
|
||||
table: "Songs",
|
||||
newName: "Url");
|
||||
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "Arist",
|
||||
table: "Songs",
|
||||
newName: "Author");
|
||||
}
|
||||
}
|
||||
}
|
135
Chiara/Chiara/Migrations/20240530030313_AddSongUrl.Designer.cs
generated
Normal file
135
Chiara/Chiara/Migrations/20240530030313_AddSongUrl.Designer.cs
generated
Normal file
|
@ -0,0 +1,135 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240530030313_AddSongUrl")]
|
||||
partial class AddSongUrl
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("ForeignId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ForeignId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
29
Chiara/Chiara/Migrations/20240530030313_AddSongUrl.cs
Normal file
29
Chiara/Chiara/Migrations/20240530030313_AddSongUrl.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddSongUrl : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Url",
|
||||
table: "Songs",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Url",
|
||||
table: "Songs");
|
||||
}
|
||||
}
|
||||
}
|
135
Chiara/Chiara/Migrations/20240601063131_UnifyFile.Designer.cs
generated
Normal file
135
Chiara/Chiara/Migrations/20240601063131_UnifyFile.Designer.cs
generated
Normal file
|
@ -0,0 +1,135 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240601063131_UnifyFile")]
|
||||
partial class UnifyFile
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
38
Chiara/Chiara/Migrations/20240601063131_UnifyFile.cs
Normal file
38
Chiara/Chiara/Migrations/20240601063131_UnifyFile.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UnifyFile : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "ForeignId",
|
||||
table: "Files",
|
||||
newName: "FileId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Files_ForeignId",
|
||||
table: "Files",
|
||||
newName: "IX_Files_FileId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.RenameColumn(
|
||||
name: "FileId",
|
||||
table: "Files",
|
||||
newName: "ForeignId");
|
||||
|
||||
migrationBuilder.RenameIndex(
|
||||
name: "IX_Files_FileId",
|
||||
table: "Files",
|
||||
newName: "IX_Files_ForeignId");
|
||||
}
|
||||
}
|
||||
}
|
139
Chiara/Chiara/Migrations/20240601065653_AlbumCoverImageUrl.Designer.cs
generated
Normal file
139
Chiara/Chiara/Migrations/20240601065653_AlbumCoverImageUrl.Designer.cs
generated
Normal file
|
@ -0,0 +1,139 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240601065653_AlbumCoverImageUrl")]
|
||||
partial class AlbumCoverImageUrl
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AlbumCoverImageUrl : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "CoverImageUrl",
|
||||
table: "Albums",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "CoverImageUrl",
|
||||
table: "Albums");
|
||||
}
|
||||
}
|
||||
}
|
144
Chiara/Chiara/Migrations/20240607080833_AddHashValue.Designer.cs
generated
Normal file
144
Chiara/Chiara/Migrations/20240607080833_AddHashValue.Designer.cs
generated
Normal file
|
@ -0,0 +1,144 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240607080833_AddHashValue")]
|
||||
partial class AddHashValue
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<byte[]>("HashValue")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
47
Chiara/Chiara/Migrations/20240607080833_AddHashValue.cs
Normal file
47
Chiara/Chiara/Migrations/20240607080833_AddHashValue.cs
Normal file
|
@ -0,0 +1,47 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddHashValue : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ContentType",
|
||||
table: "Files",
|
||||
type: "character varying(20)",
|
||||
maxLength: 20,
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
|
||||
migrationBuilder.AddColumn<byte[]>(
|
||||
name: "HashValue",
|
||||
table: "Files",
|
||||
type: "bytea",
|
||||
nullable: false,
|
||||
defaultValue: new byte[0]);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "HashValue",
|
||||
table: "Files");
|
||||
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ContentType",
|
||||
table: "Files",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(20)",
|
||||
oldMaxLength: 20);
|
||||
}
|
||||
}
|
||||
}
|
275
Chiara/Chiara/Migrations/20240608144941_ShowSeasonAndEpisodeAndMediaRepository.Designer.cs
generated
Normal file
275
Chiara/Chiara/Migrations/20240608144941_ShowSeasonAndEpisodeAndMediaRepository.Designer.cs
generated
Normal file
|
@ -0,0 +1,275 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240608144941_ShowSeasonAndEpisodeAndMediaRepository")]
|
||||
partial class ShowSeasonAndEpisodeAndMediaRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ParentRepositoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRepositoryId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<byte[]>("HashValue")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Episode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ShowSeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShowSeasonId");
|
||||
|
||||
b.ToTable("Episodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.MediaRepository", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Repositories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ParentRepositoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRepositoryId");
|
||||
|
||||
b.ToTable("Seasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.MediaRepository", "ParentRepository")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("ParentRepositoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentRepository");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Episode", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.ShowSeason", "Season")
|
||||
.WithMany("Episodes")
|
||||
.HasForeignKey("ShowSeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.MediaRepository", "ParentRepository")
|
||||
.WithMany("Seasons")
|
||||
.HasForeignKey("ParentRepositoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentRepository");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.MediaRepository", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Seasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.Navigation("Episodes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class ShowSeasonAndEpisodeAndMediaRepository : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "ParentRepositoryId",
|
||||
table: "Albums",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Repositories",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Path = table.Column<string>(type: "text", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Repositories", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Seasons",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Arist = table.Column<string>(type: "text", nullable: false),
|
||||
Path = table.Column<string>(type: "text", nullable: false),
|
||||
ParentRepositoryId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Seasons", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Seasons_Repositories_ParentRepositoryId",
|
||||
column: x => x.ParentRepositoryId,
|
||||
principalTable: "Repositories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Episodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Title = table.Column<string>(type: "text", nullable: false),
|
||||
Arist = table.Column<string>(type: "text", nullable: false),
|
||||
Path = table.Column<string>(type: "text", nullable: false),
|
||||
ShowSeasonId = table.Column<int>(type: "integer", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Episodes", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Episodes_Seasons_ShowSeasonId",
|
||||
column: x => x.ShowSeasonId,
|
||||
principalTable: "Seasons",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Albums_ParentRepositoryId",
|
||||
table: "Albums",
|
||||
column: "ParentRepositoryId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Episodes_ShowSeasonId",
|
||||
table: "Episodes",
|
||||
column: "ShowSeasonId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Seasons_ParentRepositoryId",
|
||||
table: "Seasons",
|
||||
column: "ParentRepositoryId");
|
||||
|
||||
migrationBuilder.AddForeignKey(
|
||||
name: "FK_Albums_Repositories_ParentRepositoryId",
|
||||
table: "Albums",
|
||||
column: "ParentRepositoryId",
|
||||
principalTable: "Repositories",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropForeignKey(
|
||||
name: "FK_Albums_Repositories_ParentRepositoryId",
|
||||
table: "Albums");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Episodes");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Seasons");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Repositories");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_Albums_ParentRepositoryId",
|
||||
table: "Albums");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ParentRepositoryId",
|
||||
table: "Albums");
|
||||
}
|
||||
}
|
||||
}
|
278
Chiara/Chiara/Migrations/20240609065607_AddEpisodeNumber.Designer.cs
generated
Normal file
278
Chiara/Chiara/Migrations/20240609065607_AddEpisodeNumber.Designer.cs
generated
Normal file
|
@ -0,0 +1,278 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240609065607_AddEpisodeNumber")]
|
||||
partial class AddEpisodeNumber
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ParentRepositoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRepositoryId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<byte[]>("HashValue")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Episode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("EpisodeNumber")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ShowSeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShowSeasonId");
|
||||
|
||||
b.ToTable("Episodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.MediaRepository", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Repositories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ParentRepositoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRepositoryId");
|
||||
|
||||
b.ToTable("Seasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.MediaRepository", "ParentRepository")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("ParentRepositoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentRepository");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Episode", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.ShowSeason", "Season")
|
||||
.WithMany("Episodes")
|
||||
.HasForeignKey("ShowSeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.MediaRepository", "ParentRepository")
|
||||
.WithMany("Seasons")
|
||||
.HasForeignKey("ParentRepositoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentRepository");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.MediaRepository", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Seasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.Navigation("Episodes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
29
Chiara/Chiara/Migrations/20240609065607_AddEpisodeNumber.cs
Normal file
29
Chiara/Chiara/Migrations/20240609065607_AddEpisodeNumber.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddEpisodeNumber : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "EpisodeNumber",
|
||||
table: "Episodes",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "EpisodeNumber",
|
||||
table: "Episodes");
|
||||
}
|
||||
}
|
||||
}
|
279
Chiara/Chiara/Migrations/20240611062733_UpdateEpisodeType.Designer.cs
generated
Normal file
279
Chiara/Chiara/Migrations/20240611062733_UpdateEpisodeType.Designer.cs
generated
Normal file
|
@ -0,0 +1,279 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
[Migration("20240611062733_UpdateEpisodeType")]
|
||||
partial class UpdateEpisodeType
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ParentRepositoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRepositoryId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<byte[]>("HashValue")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Episode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EpisodeNumber")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ShowSeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShowSeasonId");
|
||||
|
||||
b.ToTable("Episodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.MediaRepository", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Repositories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ParentRepositoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRepositoryId");
|
||||
|
||||
b.ToTable("Seasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.MediaRepository", "ParentRepository")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("ParentRepositoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentRepository");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Episode", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.ShowSeason", "Season")
|
||||
.WithMany("Episodes")
|
||||
.HasForeignKey("ShowSeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.MediaRepository", "ParentRepository")
|
||||
.WithMany("Seasons")
|
||||
.HasForeignKey("ParentRepositoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentRepository");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.MediaRepository", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Seasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.Navigation("Episodes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
34
Chiara/Chiara/Migrations/20240611062733_UpdateEpisodeType.cs
Normal file
34
Chiara/Chiara/Migrations/20240611062733_UpdateEpisodeType.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class UpdateEpisodeType : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "EpisodeNumber",
|
||||
table: "Episodes",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
oldClrType: typeof(int),
|
||||
oldType: "integer");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<int>(
|
||||
name: "EpisodeNumber",
|
||||
table: "Episodes",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "text");
|
||||
}
|
||||
}
|
||||
}
|
276
Chiara/Chiara/Migrations/ChiaraDbContextModelSnapshot.cs
Normal file
276
Chiara/Chiara/Migrations/ChiaraDbContextModelSnapshot.cs
Normal file
|
@ -0,0 +1,276 @@
|
|||
// <auto-generated />
|
||||
using System;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Chiara.Migrations
|
||||
{
|
||||
[DbContext(typeof(ChiaraDbContext))]
|
||||
partial class ChiaraDbContextModelSnapshot : ModelSnapshot
|
||||
{
|
||||
protected override void BuildModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "8.0.4")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ParentRepositoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRepositoryId");
|
||||
|
||||
b.ToTable("Albums");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.DatabaseFile", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<byte[]>("Content")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.Property<string>("ContentType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<Guid>("FileId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<byte[]>("HashValue")
|
||||
.IsRequired()
|
||||
.HasColumnType("bytea");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("FileId");
|
||||
|
||||
b.ToTable("Files");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Episode", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("EpisodeNumber")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ShowSeasonId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ShowSeasonId");
|
||||
|
||||
b.ToTable("Episodes");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.MediaRepository", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Repositories");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<int>("ParentRepositoryId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ParentRepositoryId");
|
||||
|
||||
b.ToTable("Seasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<int>("AlbumId")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Arist")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("CoverImageUrl")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Path")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Title")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Url")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("AlbumId");
|
||||
|
||||
b.ToTable("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.MediaRepository", "ParentRepository")
|
||||
.WithMany("Albums")
|
||||
.HasForeignKey("ParentRepositoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentRepository");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Episode", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.ShowSeason", "Season")
|
||||
.WithMany("Episodes")
|
||||
.HasForeignKey("ShowSeasonId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Season");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.MediaRepository", "ParentRepository")
|
||||
.WithMany("Seasons")
|
||||
.HasForeignKey("ParentRepositoryId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("ParentRepository");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Song", b =>
|
||||
{
|
||||
b.HasOne("Chiara.Models.Album", "Album")
|
||||
.WithMany("Songs")
|
||||
.HasForeignKey("AlbumId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Album");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.Album", b =>
|
||||
{
|
||||
b.Navigation("Songs");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.MediaRepository", b =>
|
||||
{
|
||||
b.Navigation("Albums");
|
||||
|
||||
b.Navigation("Seasons");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Chiara.Models.ShowSeason", b =>
|
||||
{
|
||||
b.Navigation("Episodes");
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
42
Chiara/Chiara/Models/Album.cs
Normal file
42
Chiara/Chiara/Models/Album.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
using Chiara.Abstractions;
|
||||
|
||||
namespace Chiara.Models;
|
||||
|
||||
public class Album : IMediaItem, IEquatable<Album>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public string Arist { get; init; } = string.Empty;
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public string CoverImageUrl { get; set; } = string.Empty;
|
||||
|
||||
public List<Song> Songs { get; set; } = [];
|
||||
|
||||
public int ParentRepositoryId { get; set; }
|
||||
|
||||
public required MediaRepository ParentRepository { get; set; }
|
||||
|
||||
public bool Equals(Album? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Title == other.Title && Arist == other.Arist;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Album other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
return Title.GetHashCode() ^ Arist.GetHashCode();
|
||||
}
|
||||
}
|
8
Chiara/Chiara/Models/ChiaraOptions.cs
Normal file
8
Chiara/Chiara/Models/ChiaraOptions.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Chiara.Models;
|
||||
|
||||
public class ChiaraOptions
|
||||
{
|
||||
public const string OptionName = "Chiara";
|
||||
|
||||
public string TemporaryDirectory { get; set; } = string.Empty;
|
||||
}
|
24
Chiara/Chiara/Models/DatabaseFile.cs
Normal file
24
Chiara/Chiara/Models/DatabaseFile.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using System.ComponentModel.DataAnnotations;
|
||||
using Chiara.Abstractions;
|
||||
|
||||
namespace Chiara.Models;
|
||||
|
||||
public class DatabaseFile : IFile
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public Guid FileId { get; set; }
|
||||
|
||||
public byte[] Content { get; set; } = [];
|
||||
|
||||
public byte[] HashValue { get; set; } = [];
|
||||
|
||||
[MaxLength(20)]
|
||||
public string ContentType { get; set; } = string.Empty;
|
||||
|
||||
public Stream OpenRead()
|
||||
{
|
||||
MemoryStream stream = new(Content);
|
||||
return stream;
|
||||
}
|
||||
}
|
37
Chiara/Chiara/Models/Episode.cs
Normal file
37
Chiara/Chiara/Models/Episode.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using Chiara.Abstractions;
|
||||
|
||||
namespace Chiara.Models;
|
||||
|
||||
public class Episode : IMediaItem, IEquatable<Episode>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public string Arist { get; init; } = string.Empty;
|
||||
|
||||
public string EpisodeNumber { get; init; } = string.Empty;
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public int ShowSeasonId { get; set; }
|
||||
|
||||
public required ShowSeason Season { get; set; }
|
||||
|
||||
public bool Equals(Episode? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Title == other.Title && Arist == other.Arist && EpisodeNumber == other.EpisodeNumber;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return obj is Episode other && Equals(other);
|
||||
}
|
||||
|
||||
public override int GetHashCode() => Title.GetHashCode() ^ Arist.GetHashCode() ^ EpisodeNumber.GetHashCode();
|
||||
}
|
17
Chiara/Chiara/Models/LocalFile.cs
Normal file
17
Chiara/Chiara/Models/LocalFile.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
using Chiara.Abstractions;
|
||||
|
||||
namespace Chiara.Models;
|
||||
|
||||
public class LocalFile(string path, string contentType) : IFile
|
||||
{
|
||||
public Guid FileId { get; } = Guid.NewGuid();
|
||||
|
||||
public FileInfo File { get; } = new(path);
|
||||
|
||||
public string ContentType { get; } = contentType;
|
||||
|
||||
public Stream OpenRead()
|
||||
{
|
||||
return File.OpenRead();
|
||||
}
|
||||
}
|
16
Chiara/Chiara/Models/MediaItemTypes.cs
Normal file
16
Chiara/Chiara/Models/MediaItemTypes.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace Chiara.Models;
|
||||
|
||||
public static class MediaItemTypes
|
||||
{
|
||||
public static readonly HashSet<string> MusicTypes =
|
||||
[
|
||||
".mp3",
|
||||
".flac"
|
||||
];
|
||||
|
||||
public static readonly HashSet<string> VideoTypes =
|
||||
[
|
||||
".mp4",
|
||||
".mkv"
|
||||
];
|
||||
}
|
14
Chiara/Chiara/Models/MediaRepository.cs
Normal file
14
Chiara/Chiara/Models/MediaRepository.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
namespace Chiara.Models;
|
||||
|
||||
public class MediaRepository
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
public List<Album> Albums { get; set; } = [];
|
||||
|
||||
public List<ShowSeason> Seasons { get; set; } = [];
|
||||
}
|
34
Chiara/Chiara/Models/ShowSeason.cs
Normal file
34
Chiara/Chiara/Models/ShowSeason.cs
Normal file
|
@ -0,0 +1,34 @@
|
|||
using Chiara.Abstractions;
|
||||
|
||||
namespace Chiara.Models;
|
||||
|
||||
public class ShowSeason : IMediaItem, IEquatable<ShowSeason>
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public string Arist { get; init; } = string.Empty;
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public List<Episode> Episodes { get; set; } = [];
|
||||
|
||||
public int ParentRepositoryId { get; set; }
|
||||
|
||||
public required MediaRepository ParentRepository { get; set; }
|
||||
|
||||
public bool Equals(ShowSeason? other)
|
||||
{
|
||||
if (other is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return Title == other.Title && Arist == other.Arist;
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is ShowSeason other && Equals(other);
|
||||
|
||||
public override int GetHashCode() => Title.GetHashCode() ^ Title.GetHashCode();
|
||||
}
|
22
Chiara/Chiara/Models/Song.cs
Normal file
22
Chiara/Chiara/Models/Song.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
using Chiara.Abstractions;
|
||||
|
||||
namespace Chiara.Models;
|
||||
|
||||
public class Song : IMediaItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Title { get; init; } = string.Empty;
|
||||
|
||||
public string Arist { get; init; } = string.Empty;
|
||||
|
||||
public string Path { get; init; } = string.Empty;
|
||||
|
||||
public string CoverImageUrl { get; init; } = string.Empty;
|
||||
|
||||
public string Url { get; init; } = string.Empty;
|
||||
|
||||
public int AlbumId { get; set; }
|
||||
|
||||
public required Album Album { get; set; }
|
||||
}
|
35
Chiara/Chiara/Program.cs
Normal file
35
Chiara/Chiara/Program.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using Chiara.Extensions;
|
||||
using Chiara.Services;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder();
|
||||
|
||||
builder.AddChiaraOptions();
|
||||
builder.Services.AddSwaggerGen();
|
||||
builder.Services.AddControllers();
|
||||
|
||||
builder.Services.AddDbContext<ChiaraDbContext>(options =>
|
||||
{
|
||||
string? connectionString = builder.Configuration.GetConnectionString("Postgres");
|
||||
if (connectionString is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to get postgres connection string.");
|
||||
}
|
||||
|
||||
options.UseNpgsql(connectionString);
|
||||
});
|
||||
builder.Services.AddChiaraService();
|
||||
|
||||
WebApplication application = builder.Build();
|
||||
|
||||
if (application.Environment.IsDevelopment())
|
||||
{
|
||||
application.UseSwagger();
|
||||
application.UseSwaggerUI();
|
||||
}
|
||||
|
||||
application.UseStaticFiles();
|
||||
|
||||
application.MapControllers();
|
||||
|
||||
await application.RunAsync();
|
23
Chiara/Chiara/Properties/launchSettings.json
Normal file
23
Chiara/Chiara/Properties/launchSettings.json
Normal file
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"$schema": "http://json.schemastore.org/launchsettings.json",
|
||||
"iisSettings": {
|
||||
"windowsAuthentication": false,
|
||||
"anonymousAuthentication": true,
|
||||
"iisExpress": {
|
||||
"applicationUrl": "http://localhost:6716",
|
||||
"sslPort": 44314
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": false,
|
||||
"launchUrl": "swagger",
|
||||
"applicationUrl": "http://0.0.0.0:5078",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
49
Chiara/Chiara/Services/ChiaraDbContext.cs
Normal file
49
Chiara/Chiara/Services/ChiaraDbContext.cs
Normal file
|
@ -0,0 +1,49 @@
|
|||
using Chiara.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Chiara.Services;
|
||||
|
||||
public class ChiaraDbContext(DbContextOptions<ChiaraDbContext> options) : DbContext(options)
|
||||
{
|
||||
public DbSet<Song> Songs { get; init; }
|
||||
|
||||
public DbSet<Album> Albums { get; init; }
|
||||
|
||||
public DbSet<DatabaseFile> Files { get; init; }
|
||||
|
||||
public DbSet<ShowSeason> Seasons { get; init; }
|
||||
|
||||
public DbSet<Episode> Episodes { get; init; }
|
||||
|
||||
public DbSet<MediaRepository> Repositories { get; set; }
|
||||
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.Entity<Album>()
|
||||
.HasMany(e => e.Songs)
|
||||
.WithOne(e => e.Album)
|
||||
.HasForeignKey(e => e.AlbumId)
|
||||
.IsRequired();
|
||||
|
||||
modelBuilder.Entity<ShowSeason>()
|
||||
.HasMany(s => s.Episodes)
|
||||
.WithOne(e => e.Season)
|
||||
.HasForeignKey(e => e.ShowSeasonId)
|
||||
.IsRequired();
|
||||
|
||||
modelBuilder.Entity<MediaRepository>()
|
||||
.HasMany(m => m.Albums)
|
||||
.WithOne(a => a.ParentRepository)
|
||||
.HasForeignKey(a => a.ParentRepositoryId)
|
||||
.IsRequired();
|
||||
|
||||
modelBuilder.Entity<MediaRepository>()
|
||||
.HasMany(m => m.Seasons)
|
||||
.WithOne(s => s.ParentRepository)
|
||||
.HasForeignKey(s => s.ParentRepositoryId)
|
||||
.IsRequired();
|
||||
|
||||
modelBuilder.Entity<DatabaseFile>()
|
||||
.HasIndex(f => f.FileId);
|
||||
}
|
||||
}
|
229
Chiara/Chiara/Services/DefaultMediaRepositoryScanner.cs
Normal file
229
Chiara/Chiara/Services/DefaultMediaRepositoryScanner.cs
Normal file
|
@ -0,0 +1,229 @@
|
|||
using System.Collections.Concurrent;
|
||||
using AnitomySharp;
|
||||
using Chiara.Abstractions;
|
||||
using Chiara.Models;
|
||||
using TagLib;
|
||||
|
||||
namespace Chiara.Services;
|
||||
|
||||
public class DefaultMediaRepositoryScanner(
|
||||
IFileStore fileStore,
|
||||
ILogger<DefaultMediaRepositoryScanner> logger) : IMediaRepositoryScanner
|
||||
{
|
||||
public async Task<IEnumerable<Album>> ScanAlbumAsync(MediaRepository mediaRepository,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
DirectoryInfo rootDirectory = new(mediaRepository.Path);
|
||||
|
||||
if (!rootDirectory.Exists)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
Dictionary<Album, List<Song>> albums = [];
|
||||
|
||||
foreach (Song song in await ScanSongAsync(rootDirectory, cancellationToken))
|
||||
{
|
||||
if (albums.TryGetValue(song.Album, out List<Song>? songs))
|
||||
{
|
||||
songs.Add(song);
|
||||
}
|
||||
else
|
||||
{
|
||||
albums.Add(song.Album, [song]);
|
||||
}
|
||||
}
|
||||
|
||||
return albums.Select(pair =>
|
||||
{
|
||||
pair.Value.ForEach(s => s.Album = pair.Key);
|
||||
|
||||
pair.Key.Songs = pair.Value;
|
||||
pair.Key.CoverImageUrl =
|
||||
Random.Shared.GetItems(pair.Key.Songs.Select(s => s.CoverImageUrl).ToArray(), 1)[0];
|
||||
|
||||
return pair.Key;
|
||||
});
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ShowSeason>> ScanShowAsync(MediaRepository repository,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
DirectoryInfo rootDirectory = new(repository.Path);
|
||||
|
||||
if (!rootDirectory.Exists)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
Dictionary<ShowSeason, List<Episode>> shows = [];
|
||||
|
||||
foreach (Episode episode in await ScanVideoAsync(rootDirectory, cancellationToken))
|
||||
{
|
||||
if (shows.TryGetValue(episode.Season, out List<Episode>? episodes))
|
||||
{
|
||||
episodes.Add(episode);
|
||||
}
|
||||
else
|
||||
{
|
||||
shows.Add(episode.Season, [episode]);
|
||||
}
|
||||
}
|
||||
|
||||
return shows.Select(pair =>
|
||||
{
|
||||
pair.Value.ForEach(e =>
|
||||
{
|
||||
e.Season = pair.Key;
|
||||
});
|
||||
|
||||
pair.Key.Episodes = pair.Value;
|
||||
return pair.Key;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Song>> ScanSongAsync(DirectoryInfo directory, CancellationToken cancellationToken)
|
||||
{
|
||||
ConcurrentBag<Song> songs = [];
|
||||
|
||||
await Parallel.ForEachAsync(directory.EnumerateDirectories(), cancellationToken, async (info, token) =>
|
||||
{
|
||||
foreach (Song song in await ScanSongAsync(info, token))
|
||||
{
|
||||
songs.Add(song);
|
||||
}
|
||||
});
|
||||
|
||||
await Parallel.ForEachAsync(directory.EnumerateFiles(), cancellationToken, async (f, _) =>
|
||||
{
|
||||
if (!MediaItemTypes.MusicTypes.Contains(f.Extension))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
TagLib.File tagFile = TagLib.File.Create(f.FullName);
|
||||
|
||||
IPicture? picture = tagFile.Tag.Pictures.FirstOrDefault();
|
||||
string? coverImageUrl = null;
|
||||
if (picture is not null)
|
||||
{
|
||||
coverImageUrl = "/api/file/" + await fileStore.UploadFileAsync(picture.Data.Data, picture.MimeType);
|
||||
}
|
||||
|
||||
Song song = new()
|
||||
{
|
||||
Title = tagFile.Tag.Title ?? f.Name,
|
||||
Arist = tagFile.Tag.FirstPerformer ?? "Default Arist",
|
||||
Path = f.FullName,
|
||||
Url = $"/api/file/{await fileStore.ClarifyLocalFileAsync(f.FullName, tagFile.MimeType)}",
|
||||
CoverImageUrl = coverImageUrl ?? string.Empty,
|
||||
Album = new Album
|
||||
{
|
||||
// 避免空应用错误
|
||||
Title = tagFile.Tag.Album ?? "Default Album",
|
||||
Arist = tagFile.Tag.FirstAlbumArtist ?? "Default Artist",
|
||||
Path = f.Directory is null ? string.Empty : f.Directory.FullName,
|
||||
ParentRepository = new MediaRepository()
|
||||
}
|
||||
};
|
||||
|
||||
songs.Add(song);
|
||||
}
|
||||
catch (UnsupportedFormatException e)
|
||||
{
|
||||
logger.LogInformation("Failed to parser file {}: {}.", f.Name, e);
|
||||
}
|
||||
});
|
||||
|
||||
return songs;
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Episode>> ScanVideoAsync(DirectoryInfo directory,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ConcurrentBag<Episode> episodes = [];
|
||||
|
||||
await Parallel.ForEachAsync(directory.EnumerateDirectories(), cancellationToken, async (d, token) =>
|
||||
{
|
||||
foreach (Episode episode in await ScanVideoAsync(d, token))
|
||||
{
|
||||
episodes.Add(episode);
|
||||
}
|
||||
});
|
||||
|
||||
await Parallel.ForEachAsync(directory.EnumerateFiles(), cancellationToken, async (f, _) =>
|
||||
{
|
||||
if (!MediaItemTypes.VideoTypes.Contains(f.Extension))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
List<Element> elements = Anitomy.Parse(f.Name).ToList();
|
||||
|
||||
Element? titleElement = (from item in elements
|
||||
where item.Category == ElementCategory.ElementAnimeTitle
|
||||
select item).FirstOrDefault();
|
||||
|
||||
if (titleElement is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Element? episodeNumberElement = (from item in elements
|
||||
where item.Category == ElementCategory.ElementEpisodeNumber
|
||||
select item).FirstOrDefault();
|
||||
|
||||
if (episodeNumberElement is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Element? episodeTitleElement = (from item in elements
|
||||
where item.Category == ElementCategory.ElementEpisodeTitle
|
||||
select item).FirstOrDefault();
|
||||
|
||||
if (episodeTitleElement is null)
|
||||
{
|
||||
Episode episode = new()
|
||||
{
|
||||
Title = $"{titleElement.Value} E{episodeNumberElement.Value}",
|
||||
Arist = string.Empty,
|
||||
Path = f.FullName,
|
||||
EpisodeNumber = episodeNumberElement.Value,
|
||||
Season = new ShowSeason
|
||||
{
|
||||
Title = titleElement.Value,
|
||||
Arist = string.Empty,
|
||||
Path = f.Directory?.FullName ?? string.Empty,
|
||||
ParentRepository = new MediaRepository()
|
||||
}
|
||||
};
|
||||
|
||||
episodes.Add(episode);
|
||||
}
|
||||
else
|
||||
{
|
||||
Episode episode = new()
|
||||
{
|
||||
Title = episodeTitleElement.Value,
|
||||
Arist = string.Empty,
|
||||
Path = f.FullName,
|
||||
EpisodeNumber = episodeNumberElement.Value,
|
||||
Season = new ShowSeason
|
||||
{
|
||||
Title = titleElement.Value,
|
||||
Arist = string.Empty,
|
||||
Path = f.Directory?.FullName ?? string.Empty,
|
||||
ParentRepository = new MediaRepository()
|
||||
}
|
||||
};
|
||||
|
||||
episodes.Add(episode);
|
||||
}
|
||||
});
|
||||
|
||||
return episodes;
|
||||
}
|
||||
}
|
30
Chiara/Chiara/Services/FfmpegService.cs
Normal file
30
Chiara/Chiara/Services/FfmpegService.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
using Xabe.FFmpeg;
|
||||
|
||||
namespace Chiara.Services;
|
||||
|
||||
public record VideoConversion(Task<IConversionResult> RunningTask,
|
||||
DirectoryInfo CacheDirectory,
|
||||
string Name,
|
||||
CancellationTokenSource CancellationTokenSource);
|
||||
|
||||
public class FfmpegService
|
||||
{
|
||||
public VideoConversion? CurrentConversion { get; set; }
|
||||
|
||||
public async Task<IConversionResult> StartConversion(FileInfo video, DirectoryInfo cacheDirectory, string name,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
IMediaInfo inputVideo = await FFmpeg.GetMediaInfo(video.FullName, cancellationToken);
|
||||
|
||||
IConversion conversion = FFmpeg.Conversions.New()
|
||||
.AddStream(inputVideo.Streams)
|
||||
.AddParameter("-re", ParameterPosition.PreInput)
|
||||
.AddParameter("-c:v h264 -f hls")
|
||||
.AddParameter("-profile:v high10")
|
||||
.AddParameter("-hls_list_size 10 -hls_time 10 -hls_base_url /api/hls/")
|
||||
.AddParameter("-hls_flags delete_segments")
|
||||
.SetOutput(Path.Combine(cacheDirectory.FullName, $"{name}.m3u8"));
|
||||
|
||||
return await conversion.Start(cancellationToken);
|
||||
}
|
||||
}
|
88
Chiara/Chiara/Services/FileService.cs
Normal file
88
Chiara/Chiara/Services/FileService.cs
Normal file
|
@ -0,0 +1,88 @@
|
|||
using System.Security.Cryptography;
|
||||
using System.Threading.Channels;
|
||||
using Chiara.Abstractions;
|
||||
using Chiara.Models;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Chiara.Services;
|
||||
|
||||
public sealed class FileService(
|
||||
IServiceProvider serviceProvider,
|
||||
LocalFileService localFileService,
|
||||
ILogger<FileService> logger)
|
||||
: BackgroundService, IFileStore
|
||||
{
|
||||
private readonly Channel<DatabaseFile> _channel = Channel.CreateUnbounded<DatabaseFile>(new UnboundedChannelOptions
|
||||
{
|
||||
SingleReader = true
|
||||
});
|
||||
|
||||
public async Task<string> UploadFileAsync(ReadOnlyMemory<byte> buffer, string contentType)
|
||||
{
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
await using ChiaraDbContext dbContent = scope.ServiceProvider.GetRequiredService<ChiaraDbContext>();
|
||||
|
||||
await using Stream stream = new MemoryStream();
|
||||
await stream.WriteAsync(buffer);
|
||||
stream.Position = 0;
|
||||
|
||||
using HMACSHA256 hmacsha256 = new([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
byte[] hash = await hmacsha256.ComputeHashAsync(stream);
|
||||
|
||||
DatabaseFile? existedFile = await (from item in dbContent.Files.AsNoTracking()
|
||||
where item.HashValue == hash && item.ContentType == contentType
|
||||
select item).FirstOrDefaultAsync();
|
||||
|
||||
if (existedFile is not null)
|
||||
{
|
||||
return existedFile.FileId.ToString();
|
||||
}
|
||||
|
||||
DatabaseFile file = new()
|
||||
{
|
||||
FileId = Guid.NewGuid(), Content = buffer.ToArray(), ContentType = contentType, HashValue = hash
|
||||
};
|
||||
|
||||
await _channel.Writer.WriteAsync(file);
|
||||
|
||||
return file.FileId.ToString();
|
||||
}
|
||||
|
||||
public Task<string> ClarifyLocalFileAsync(string path, string contentType)
|
||||
=> localFileService.ClarifyFile(path, contentType);
|
||||
|
||||
public async Task<IFile?> GetFileAsync(Guid guid)
|
||||
{
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
await using ChiaraDbContext dbContext = scope.ServiceProvider.GetRequiredService<ChiaraDbContext>();
|
||||
|
||||
DatabaseFile? file = await (from item in dbContext.Files.AsNoTracking()
|
||||
where item.FileId == guid
|
||||
select item).FirstOrDefaultAsync();
|
||||
|
||||
if (file is not null)
|
||||
{
|
||||
return file;
|
||||
}
|
||||
|
||||
return localFileService.ReadFile(guid);
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
await using ChiaraDbContext dbContext = scope.ServiceProvider.GetRequiredService<ChiaraDbContext>();
|
||||
|
||||
while (await _channel.Reader.WaitToReadAsync(stoppingToken))
|
||||
{
|
||||
if (!_channel.Reader.TryRead(out DatabaseFile? file))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
logger.LogInformation("Receive file from channel: {}.", file.FileId);
|
||||
await dbContext.Files.AddAsync(file, stoppingToken);
|
||||
await dbContext.SaveChangesAsync(stoppingToken);
|
||||
}
|
||||
}
|
||||
}
|
36
Chiara/Chiara/Services/LocalFileService.cs
Normal file
36
Chiara/Chiara/Services/LocalFileService.cs
Normal file
|
@ -0,0 +1,36 @@
|
|||
using System.Collections.Concurrent;
|
||||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.Services;
|
||||
|
||||
public class LocalFileService
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, LocalFile> _idMap = [];
|
||||
|
||||
private readonly ConcurrentDictionary<string, LocalFile> _pathMap = [];
|
||||
|
||||
public Task<string> ClarifyFile(string path, string contentType)
|
||||
{
|
||||
LocalFile localFile = new(path, contentType);
|
||||
|
||||
if (!localFile.File.Exists)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
if (_pathMap.TryGetValue(localFile.File.FullName, out LocalFile? existedFile))
|
||||
{
|
||||
return Task.FromResult(existedFile.FileId.ToString());
|
||||
}
|
||||
|
||||
_idMap.TryAdd(localFile.FileId, localFile);
|
||||
_pathMap.TryAdd(localFile.File.FullName, localFile);
|
||||
|
||||
return Task.FromResult(localFile.FileId.ToString());
|
||||
}
|
||||
|
||||
public LocalFile? ReadFile(Guid guid)
|
||||
{
|
||||
return _idMap.GetValueOrDefault(guid);
|
||||
}
|
||||
}
|
44
Chiara/Chiara/Services/MigrationService.cs
Normal file
44
Chiara/Chiara/Services/MigrationService.cs
Normal file
|
@ -0,0 +1,44 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Storage;
|
||||
|
||||
namespace Chiara.Services;
|
||||
|
||||
public class MigrationService(IServiceProvider serviceProvider) : BackgroundService
|
||||
{
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
using IServiceScope scope = serviceProvider.CreateScope();
|
||||
ChiaraDbContext dbContext = scope.ServiceProvider.GetRequiredService<ChiaraDbContext>();
|
||||
|
||||
await EnsureDatabaseAsync(dbContext, stoppingToken);
|
||||
await RunMigrationAsync(dbContext, stoppingToken);
|
||||
}
|
||||
|
||||
private static async Task EnsureDatabaseAsync(ChiaraDbContext dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
IRelationalDatabaseCreator creator = dbContext.GetService<IRelationalDatabaseCreator>();
|
||||
IExecutionStrategy strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
if (!await creator.ExistsAsync(cancellationToken))
|
||||
{
|
||||
await creator.CreateAsync(cancellationToken);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task RunMigrationAsync(ChiaraDbContext dbContext, CancellationToken cancellationToken)
|
||||
{
|
||||
IExecutionStrategy strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using IDbContextTransaction transaction =
|
||||
await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
await dbContext.Database.MigrateAsync(cancellationToken);
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
});
|
||||
}
|
||||
}
|
45
Chiara/Chiara/Services/RefreshService.cs
Normal file
45
Chiara/Chiara/Services/RefreshService.cs
Normal file
|
@ -0,0 +1,45 @@
|
|||
using Chiara.Abstractions;
|
||||
using Chiara.Models;
|
||||
|
||||
namespace Chiara.Services;
|
||||
|
||||
public class RefreshService(
|
||||
ChiaraDbContext dbContext,
|
||||
IMediaRepositoryScanner scanner)
|
||||
{
|
||||
public async Task Refresh(MediaRepository repository)
|
||||
{
|
||||
dbContext.Albums.RemoveRange(repository.Albums);
|
||||
dbContext.Seasons.RemoveRange(repository.Seasons);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
IEnumerable<Album> albums = await scanner.ScanAlbumAsync(repository);
|
||||
|
||||
foreach (Album album in albums)
|
||||
{
|
||||
List<Song> songs = album.Songs;
|
||||
album.Songs = [];
|
||||
repository.Albums.Add(album);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
songs.ForEach(s => s.AlbumId = album.Id);
|
||||
|
||||
await dbContext.Songs.AddRangeAsync(songs);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
IEnumerable<ShowSeason> seasons = await scanner.ScanShowAsync(repository);
|
||||
|
||||
foreach (ShowSeason season in seasons)
|
||||
{
|
||||
List<Episode> episodes = season.Episodes;
|
||||
season.Episodes = [];
|
||||
repository.Seasons.Add(season);
|
||||
await dbContext.SaveChangesAsync();
|
||||
|
||||
episodes.ForEach(s => s.ShowSeasonId = season.Id);
|
||||
await dbContext.Episodes.AddRangeAsync(episodes);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
}
|
14
Chiara/Chiara/appsettings.Development.json
Normal file
14
Chiara/Chiara/appsettings.Development.json
Normal file
|
@ -0,0 +1,14 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"Postgres": "Host=localhost;Database=chiara;Username=postgres;Password=12345678"
|
||||
},
|
||||
"Repository": {
|
||||
"Path": "/home/ricardo/Documents/Code/CSharp/Chiara/Chiara.Tests/Data"
|
||||
}
|
||||
}
|
9
Chiara/Chiara/appsettings.json
Normal file
9
Chiara/Chiara/appsettings.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
28
Chiara/Chiara/docker-compose.yaml
Normal file
28
Chiara/Chiara/docker-compose.yaml
Normal file
|
@ -0,0 +1,28 @@
|
|||
version: "3.9"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: postgres:15
|
||||
container_name: chiara-db
|
||||
environment:
|
||||
POSTGRES_PASSWORD: 12345678
|
||||
|
||||
chiara:
|
||||
image: chiara:latest
|
||||
container_name: chiara-app
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
TZ: "Asia/Shanghai"
|
||||
ASPNETCORE_ENVIRONMENT: Development
|
||||
ConnectionStrings__Postgres: "Host=db;Database=chiara;Username=postgres;Password=12345678"
|
||||
Repository__Path: "/data"
|
||||
Chiara__TemporaryDirectory: "/tmp/"
|
||||
volumes:
|
||||
- /home/ricardo/Documents/Code/CSharp/Chiara/Chiara.Tests/Data:/data/music
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.chiara.rule=Host(`chiara.jackfiled.icu`)"
|
||||
- "traefik.http.services.chiara.loadbalancer.server.port=8080"
|
||||
- "com.centurylinklabs.watchtower.enable=true"
|
||||
|
0
Chiara/Chiara/wwwroot/.gitkeep
Normal file
0
Chiara/Chiara/wwwroot/.gitkeep
Normal file
15
ChiaraAndroid/.gitignore
vendored
Normal file
15
ChiaraAndroid/.gitignore
vendored
Normal file
|
@ -0,0 +1,15 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.externalNativeBuild
|
||||
.cxx
|
||||
local.properties
|
3
ChiaraAndroid/.idea/.gitignore
vendored
Normal file
3
ChiaraAndroid/.idea/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
1
ChiaraAndroid/.idea/.name
Normal file
1
ChiaraAndroid/.idea/.name
Normal file
|
@ -0,0 +1 @@
|
|||
Chiara
|
6
ChiaraAndroid/.idea/compiler.xml
Normal file
6
ChiaraAndroid/.idea/compiler.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="CompilerConfiguration">
|
||||
<bytecodeTargetLevel target="17" />
|
||||
</component>
|
||||
</project>
|
18
ChiaraAndroid/.idea/deploymentTargetSelector.xml
Normal file
18
ChiaraAndroid/.idea/deploymentTargetSelector.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetSelector">
|
||||
<selectionStates>
|
||||
<SelectionState runConfigName="app">
|
||||
<option name="selectionMode" value="DROPDOWN" />
|
||||
<DropdownSelection timestamp="2024-06-10T03:24:59.011863144Z">
|
||||
<Target type="DEFAULT_BOOT">
|
||||
<handle>
|
||||
<DeviceId pluginId="PhysicalDevice" identifier="serial=3040223527000PT" />
|
||||
</handle>
|
||||
</Target>
|
||||
</DropdownSelection>
|
||||
<DialogSelection />
|
||||
</SelectionState>
|
||||
</selectionStates>
|
||||
</component>
|
||||
</project>
|
19
ChiaraAndroid/.idea/gradle.xml
Normal file
19
ChiaraAndroid/.idea/gradle.xml
Normal file
|
@ -0,0 +1,19 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveExternalAnnotations" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
41
ChiaraAndroid/.idea/inspectionProfiles/Project_Default.xml
Normal file
41
ChiaraAndroid/.idea/inspectionProfiles/Project_Default.xml
Normal file
|
@ -0,0 +1,41 @@
|
|||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewApiLevelMustBeValid" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
</profile>
|
||||
</component>
|
6
ChiaraAndroid/.idea/kotlinc.xml
Normal file
6
ChiaraAndroid/.idea/kotlinc.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.0" />
|
||||
</component>
|
||||
</project>
|
10
ChiaraAndroid/.idea/migrations.xml
Normal file
10
ChiaraAndroid/.idea/migrations.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectMigrations">
|
||||
<option name="MigrateToGradleLocalJavaHome">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
9
ChiaraAndroid/.idea/misc.xml
Normal file
9
ChiaraAndroid/.idea/misc.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<project version="4">
|
||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
6
ChiaraAndroid/.idea/vcs.xml
Normal file
6
ChiaraAndroid/.idea/vcs.xml
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
1
ChiaraAndroid/app/.gitignore
vendored
Normal file
1
ChiaraAndroid/app/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
152
ChiaraAndroid/app/build.gradle.kts
Normal file
152
ChiaraAndroid/app/build.gradle.kts
Normal file
|
@ -0,0 +1,152 @@
|
|||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.jetbrains.kotlin.android)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
alias(libs.plugins.hilt.android)
|
||||
alias(libs.plugins.kotlin.ksp)
|
||||
alias(libs.plugins.openapi.generator)
|
||||
id("kotlin-kapt")
|
||||
}
|
||||
|
||||
val openapiOutputDir: String = layout.buildDirectory.dir("generated/api").get().asFile.path
|
||||
|
||||
android {
|
||||
namespace = "top.rrricardo.chiara"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "top.rrricardo.chiara"
|
||||
minSdk = 28
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
sourceSets["main"].kotlin {
|
||||
srcDir("$openapiOutputDir/src/main/kotlin")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.1"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
// Default
|
||||
implementation(libs.androidx.core.ktx)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
implementation(libs.androidx.activity.compose)
|
||||
implementation(platform(libs.androidx.compose.bom))
|
||||
implementation(libs.androidx.ui)
|
||||
implementation(libs.androidx.ui.graphics)
|
||||
implementation(libs.androidx.ui.tooling.preview)
|
||||
implementation(libs.androidx.material3)
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||
debugImplementation(libs.androidx.ui.tooling)
|
||||
debugImplementation(libs.androidx.ui.test.manifest)
|
||||
|
||||
// ViewModel
|
||||
implementation(libs.androidx.lifecycle.viewmodel.ktx)
|
||||
// ViewModel utilities for Compose
|
||||
implementation(libs.androidx.lifecycle.viewmodel.compose)
|
||||
// LiveData
|
||||
implementation(libs.androidx.lifecycle.livedata.ktx)
|
||||
// Lifecycles only (without ViewModel or LiveData)
|
||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||
// Lifecycle utilities for Compose
|
||||
implementation(libs.androidx.lifecycle.runtime.compose)
|
||||
|
||||
// Saved state module for ViewModel
|
||||
implementation(libs.androidx.lifecycle.viewmodel.savedstate)
|
||||
|
||||
// Annotation processor
|
||||
kapt(libs.androidx.lifecycle.compiler)
|
||||
|
||||
// Navigation Controller
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
// Palette
|
||||
implementation(libs.androidx.palette)
|
||||
|
||||
// Media Exo Player
|
||||
implementation(libs.media3.exoplayer)
|
||||
implementation(libs.media3.session)
|
||||
implementation(libs.media3.ui)
|
||||
implementation(libs.media3.hls)
|
||||
|
||||
// Hilt
|
||||
implementation(libs.hilt.android)
|
||||
implementation(libs.hilt.navigation.compose)
|
||||
kapt(libs.hilt.android.compiler)
|
||||
|
||||
// Constraint Layout
|
||||
implementation(libs.constraintlayout.compose)
|
||||
|
||||
// Coil
|
||||
implementation(libs.coil)
|
||||
|
||||
// openapi
|
||||
implementation(libs.okhttp3)
|
||||
implementation(libs.retrofit.retrofit)
|
||||
implementation(libs.retrofit.converter.scalars)
|
||||
implementation(libs.retrofit.serialization.converter)
|
||||
implementation(libs.kotlinx.serialization)
|
||||
}
|
||||
|
||||
|
||||
openApiGenerate {
|
||||
generatorName.set("kotlin")
|
||||
inputSpec.set("$rootDir/app/src/main/openapi/chiara.json")
|
||||
outputDir.set(openapiOutputDir)
|
||||
apiPackage.set("top.rrricardo.chiara.openapi.api")
|
||||
modelPackage.set("top.rrricardo.chiara.openapi.model")
|
||||
packageName.set("top.rrricardo.chiara.openapi.client")
|
||||
generateApiTests.set(false)
|
||||
generateModelTests.set(false)
|
||||
configOptions.set(
|
||||
mapOf(
|
||||
"dataLibrary" to "java8"
|
||||
)
|
||||
)
|
||||
additionalProperties.set(
|
||||
mapOf(
|
||||
"library" to "jvm-retrofit2",
|
||||
"serializationLibrary" to "kotlinx_serialization",
|
||||
"useCoroutines" to "true",
|
||||
)
|
||||
)
|
||||
}
|
21
ChiaraAndroid/app/proguard-rules.pro
vendored
Normal file
21
ChiaraAndroid/app/proguard-rules.pro
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
|
@ -0,0 +1,24 @@
|
|||
package top.rrricardo.chiara
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* See [testing documentation](http://d.android.com/tools/testing).
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class ExampleInstrumentedTest {
|
||||
@Test
|
||||
fun useAppContext() {
|
||||
// Context of the app under test.
|
||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
assertEquals("top.rrricardo.chiara", appContext.packageName)
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user