From b3ae68232d7d70ec5a5d66f6a621bb790a8c61a8 Mon Sep 17 00:00:00 2001 From: Mike Nolan Date: Sun, 28 Jul 2024 17:59:28 -0500 Subject: [PATCH] Fixed video player in app for desktop --- .gitignore | 3 +- Dockerfile | 4 +- Tesses.CMS.Avalonia/.gitignore | 454 ++ Tesses.CMS.Avalonia/Directory.Build.props | 6 + .../Tesses.CMS.Avalonia.Android/Icon.png | Bin 0 -> 14349 bytes .../MainActivity.cs | 41 + .../MobilePlatform.cs | 92 + .../Properties/AndroidManifest.xml | 5 + .../Resources/AboutResources.txt | 44 + .../drawable-night-v31/avalonia_anim.xml | 66 + .../Resources/drawable-v31/avalonia_anim.xml | 71 + .../Resources/drawable/splash_screen.xml | 13 + .../Resources/values-night/colors.xml | 4 + .../Resources/values-v31/styles.xml | 21 + .../Resources/values/colors.xml | 4 + .../Resources/values/styles.xml | 12 + .../SAFFileStream.cs | 531 ++ .../Tesses.CMS.Avalonia.Android.csproj | 29 + .../DesktopPlatform.cs | 95 + .../Tesses.CMS.Avalonia.Desktop/Program.cs | 28 + .../Tesses.CMS.Avalonia.Desktop.csproj | 28 + .../VideoPlayer.cs | 124 + .../Tesses.CMS.Avalonia.Desktop/app.manifest | 18 + Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.sln | 50 + .../Tesses.CMS.Avalonia/App.axaml | 15 + .../Tesses.CMS.Avalonia/App.axaml.cs | 160 + .../Assets/avalonia-logo.ico | Bin 0 -> 176111 bytes .../Assets/no-media-icon.png | Bin 0 -> 1373 bytes .../Tesses.CMS.Avalonia/Configuration.cs | 12 + .../Tesses.CMS.Avalonia/IPlatform.cs | 70 + .../Tesses.CMS.Avalonia/Models/MovieItem.cs | 16 + .../Tesses.CMS.Avalonia/Models/Page.cs | 16 + .../Tesses.CMS.Avalonia.csproj | 29 + .../Tesses.CMS.Avalonia/ViewLocator.cs | 30 + .../ViewModels/DownloadsPageViewModel.cs | 19 + .../ViewModels/FavoritesPageViewModel.cs | 8 + .../ViewModels/HomePageViewModel.cs | 29 + .../HomePages/HomeMovieListPageViewModel.cs | 53 + .../HomePages/HomeUserListPageViewModel.cs | 37 + .../HomePages/HomeUserPageViewModel.cs | 55 + .../ViewModels/IBackable.cs | 6 + .../ViewModels/MainViewModel.cs | 60 + .../ViewModels/NotificationsPageViewModel.cs | 8 + .../ViewModels/SettingsPageViewModel.cs | 50 + .../ViewModels/ViewModelBase.cs | 8 + .../Views/DownloadsPageView.axaml | 17 + .../Views/DownloadsPageView.axaml.cs | 16 + .../Views/FavoritesPageView.axaml | 15 + .../Views/FavoritesPageView.axaml.cs | 11 + .../Views/HomeMovieListPageView.axaml | 24 + .../Views/HomeMovieListPageView.axaml.cs | 11 + .../Views/HomePageView.axaml | 18 + .../Views/HomePageView.axaml.cs | 11 + .../Views/HomeUserListPageView.axaml | 23 + .../Views/HomeUserListPageView.axaml.cs | 11 + .../Views/HomeUserPageView.axaml | 23 + .../Views/HomeUserPageView.axaml.cs | 11 + .../Tesses.CMS.Avalonia/Views/MainView.axaml | 44 + .../Views/MainView.axaml.cs | 11 + .../Views/MainWindow.axaml | 12 + .../Views/MainWindow.axaml.cs | 11 + .../Views/NotificationsPageView.axaml | 15 + .../Views/NotificationsPageView.axaml.cs | 11 + .../Views/SettingsPageView.axaml | 34 + .../Views/SettingsPageView.axaml.cs | 11 + .../Views/VideoPlayerWrapper.cs | 41 + Tesses.CMS.Cli/Program.cs | 221 +- Tesses.CMS.Cli/Tesses.CMS.Cli.csproj | 5 + Tesses.CMS.Client/Class1.cs | 495 +- Tesses.CMS.Client/Episode.cs | 25 + Tesses.CMS.Client/Movie.cs | 24 +- Tesses.CMS.Client/SSEClient.cs | 69 + Tesses.CMS.Client/Season.cs | 22 + Tesses.CMS.Client/SeasonWithEpisodes.cs | 31 + Tesses.CMS.Client/Show.cs | 20 + .../ShowWithSeasonsAndEpisodes.cs | 29 + Tesses.CMS.Client/UserAccount.cs | 14 + Tesses.CMS.Providers.Dapper/Class1.cs | 60 + Tesses.CMS.Providers.LiteDb/Class1.cs | 161 +- Tesses.CMS.Server/Tesses.CMS.Server.csproj | 2 +- Tesses.CMS/Album.cs | 43 + Tesses.CMS/AssetProvier.cs | 2 +- Tesses.CMS/Assets/AddMovie.html | 0 Tesses.CMS/Assets/AlbumPage.html | 50 + Tesses.CMS/Assets/AlbumsPage.html | 24 + Tesses.CMS/Assets/Devcenter.html | 1 - Tesses.CMS/Assets/EditAlbumDetails.html | 35 + Tesses.CMS/Assets/EditEpisodeDetails.html | 28 + Tesses.CMS/Assets/EditMovieDetails.html | 4 +- Tesses.CMS/Assets/EditSeasonDetails.html | 4 +- Tesses.CMS/Assets/EmailAlbum.html | 10 + Tesses.CMS/Assets/EmailMovie.html | 9 + Tesses.CMS/Assets/EmailShow.html | 10 + Tesses.CMS/Assets/EpisodePage.html | 38 + Tesses.CMS/Assets/ExtrasViewer.html | 4 +- Tesses.CMS/Assets/MailingList.html | 14 +- Tesses.CMS/Assets/ManageHtml.html | 2 +- Tesses.CMS/Assets/MoviePage.html | 10 +- Tesses.CMS/Assets/MusicPlayerPage.html | 289 + Tesses.CMS/Assets/PageShell.html | 4 + Tesses.CMS/Assets/ShowPage.html | 9 + Tesses.CMS/Assets/Tracklist.html | 17 + Tesses.CMS/Assets/Upload.html | 15 +- Tesses.CMS/Assets/UserPage.html | 18 +- Tesses.CMS/Assets/WatchEpisode.html | 129 + Tesses.CMS/Assets/Webhook.html | 108 + Tesses.CMS/Assets/android-chrome-192x192.png | Bin 0 -> 11528 bytes Tesses.CMS/Assets/android-chrome-512x512.png | Bin 0 -> 52650 bytes Tesses.CMS/Assets/apple-touch-icon.png | Bin 0 -> 13321 bytes Tesses.CMS/Assets/download.svg | 1 + Tesses.CMS/Assets/favicon-16x16.png | Bin 0 -> 617 bytes Tesses.CMS/Assets/favicon-32x32.png | Bin 0 -> 1196 bytes Tesses.CMS/Assets/favicon.ico | Bin 0 -> 318 bytes Tesses.CMS/Assets/pause.svg | 1 + Tesses.CMS/Assets/play.svg | 1 + Tesses.CMS/Assets/repeat_all.svg | 1 + Tesses.CMS/Assets/repeat_off.svg | 1 + Tesses.CMS/Assets/repeat_one.svg | 1 + Tesses.CMS/Assets/shuffle_off.svg | 1 + Tesses.CMS/Assets/shuffle_on.svg | 1 + Tesses.CMS/Assets/site.webmanifest | 1 + Tesses.CMS/Assets/skip-next.svg | 1 + Tesses.CMS/Assets/skip-prev.svg | 1 + Tesses.CMS/CMSConfiguration.cs | 2 + Tesses.CMS/CSRF.cs | 29 + Tesses.CMS/Class1.cs | 5153 +++++++++++++---- Tesses.CMS/Episode.cs | 14 +- Tesses.CMS/Event.cs | 42 + Tesses.CMS/IContentProvider.cs | 140 +- Tesses.CMS/Movie.cs | 9 +- Tesses.CMS/Project.cs | 40 + Tesses.CMS/ProjectRelease.cs | 141 + Tesses.CMS/Season.cs | 9 +- Tesses.CMS/Show.cs | 9 +- Tesses.CMS/Tesses.CMS.csproj | 2 +- Tesses.CMS/UserAccount.cs | 40 +- 136 files changed, 9375 insertions(+), 1126 deletions(-) create mode 100644 Tesses.CMS.Avalonia/.gitignore create mode 100644 Tesses.CMS.Avalonia/Directory.Build.props create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Icon.png create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MainActivity.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MobilePlatform.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Properties/AndroidManifest.xml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/AboutResources.txt create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable-night-v31/avalonia_anim.xml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable-v31/avalonia_anim.xml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable/splash_screen.xml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values-night/colors.xml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values-v31/styles.xml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values/colors.xml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values/styles.xml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/SAFFileStream.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Tesses.CMS.Avalonia.Android.csproj create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/DesktopPlatform.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Program.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Tesses.CMS.Avalonia.Desktop.csproj create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/VideoPlayer.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/app.manifest create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.sln create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/avalonia-logo.ico create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/no-media-icon.png create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Configuration.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/IPlatform.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/MovieItem.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/Page.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.csproj create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewLocator.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/DownloadsPageViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/FavoritesPageViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePageViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieListPageViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserListPageViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserPageViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/IBackable.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/MainViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/NotificationsPageViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/SettingsPageViewModel.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ViewModelBase.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/DownloadsPageView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/DownloadsPageView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/FavoritesPageView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/FavoritesPageView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeMovieListPageView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeMovieListPageView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomePageView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomePageView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserListPageView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserListPageView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserPageView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserPageView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainWindow.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainWindow.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/NotificationsPageView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/NotificationsPageView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/SettingsPageView.axaml create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/SettingsPageView.axaml.cs create mode 100644 Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/VideoPlayerWrapper.cs create mode 100644 Tesses.CMS.Client/Episode.cs create mode 100644 Tesses.CMS.Client/SSEClient.cs create mode 100644 Tesses.CMS.Client/Season.cs create mode 100644 Tesses.CMS.Client/SeasonWithEpisodes.cs create mode 100644 Tesses.CMS.Client/Show.cs create mode 100644 Tesses.CMS.Client/ShowWithSeasonsAndEpisodes.cs create mode 100644 Tesses.CMS.Client/UserAccount.cs create mode 100644 Tesses.CMS/Album.cs delete mode 100644 Tesses.CMS/Assets/AddMovie.html create mode 100644 Tesses.CMS/Assets/AlbumPage.html create mode 100644 Tesses.CMS/Assets/AlbumsPage.html create mode 100644 Tesses.CMS/Assets/EditAlbumDetails.html create mode 100644 Tesses.CMS/Assets/EditEpisodeDetails.html create mode 100644 Tesses.CMS/Assets/EmailAlbum.html create mode 100644 Tesses.CMS/Assets/EmailMovie.html create mode 100644 Tesses.CMS/Assets/EmailShow.html create mode 100644 Tesses.CMS/Assets/EpisodePage.html create mode 100644 Tesses.CMS/Assets/MusicPlayerPage.html create mode 100644 Tesses.CMS/Assets/Tracklist.html create mode 100644 Tesses.CMS/Assets/WatchEpisode.html create mode 100644 Tesses.CMS/Assets/Webhook.html create mode 100644 Tesses.CMS/Assets/android-chrome-192x192.png create mode 100644 Tesses.CMS/Assets/android-chrome-512x512.png create mode 100644 Tesses.CMS/Assets/apple-touch-icon.png create mode 100644 Tesses.CMS/Assets/download.svg create mode 100644 Tesses.CMS/Assets/favicon-16x16.png create mode 100644 Tesses.CMS/Assets/favicon-32x32.png create mode 100644 Tesses.CMS/Assets/favicon.ico create mode 100644 Tesses.CMS/Assets/pause.svg create mode 100644 Tesses.CMS/Assets/play.svg create mode 100644 Tesses.CMS/Assets/repeat_all.svg create mode 100644 Tesses.CMS/Assets/repeat_off.svg create mode 100644 Tesses.CMS/Assets/repeat_one.svg create mode 100644 Tesses.CMS/Assets/shuffle_off.svg create mode 100644 Tesses.CMS/Assets/shuffle_on.svg create mode 100644 Tesses.CMS/Assets/site.webmanifest create mode 100644 Tesses.CMS/Assets/skip-next.svg create mode 100644 Tesses.CMS/Assets/skip-prev.svg create mode 100644 Tesses.CMS/CSRF.cs create mode 100644 Tesses.CMS/Event.cs create mode 100644 Tesses.CMS/Project.cs create mode 100644 Tesses.CMS/ProjectRelease.cs diff --git a/.gitignore b/.gitignore index d721b24..fc25db9 100644 --- a/.gitignore +++ b/.gitignore @@ -482,4 +482,5 @@ $RECYCLE.BIN/ # Vim temporary swap files *.swp -help.txt \ No newline at end of file +help.txt +data/ diff --git a/Dockerfile b/Dockerfile index dfc6bd6..fcccaeb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY . . WORKDIR /src/Tesses.CMS.Server RUN dotnet publish -c Release -o /app -FROM mcr.microsoft.com/dotnet/runtime:7.0 AS runner +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS runner RUN apt update && apt install -y ffmpeg WORKDIR /app COPY --from=build /app . diff --git a/Tesses.CMS.Avalonia/.gitignore b/Tesses.CMS.Avalonia/.gitignore new file mode 100644 index 0000000..48cc008 --- /dev/null +++ b/Tesses.CMS.Avalonia/.gitignore @@ -0,0 +1,454 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# 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 Core +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 +*.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 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/ + +# 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 + +## +## 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 + +# JetBrains Rider +.idea/ +*.sln.iml + +## +## Visual Studio Code +## +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/Tesses.CMS.Avalonia/Directory.Build.props b/Tesses.CMS.Avalonia/Directory.Build.props new file mode 100644 index 0000000..f39656c --- /dev/null +++ b/Tesses.CMS.Avalonia/Directory.Build.props @@ -0,0 +1,6 @@ + + + enable + 11.0.6 + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Icon.png b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Icon.png new file mode 100644 index 0000000000000000000000000000000000000000..41a2a618fb02e4cb7f6a15caf572b693bfe1ebb1 GIT binary patch literal 14349 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 +{ + protected override AppBuilder CustomizeAppBuilder(AppBuilder builder) + { + App.Platform = new MobilePlatform(this); + return base.CustomizeAppBuilder(builder) + .WithInterFont() + .UseReactiveUI(); + } + protected override void OnActivityResult(int requestCode, [GeneratedEnum] Result resultCode, Intent? data) + { + if(requestCode == SAFFileSystem.RequestCode && resultCode == Result.Ok) + { + var res=SAFFileSystem.GetSAFFromResponsePresistant(this,data,MobilePlatform.MySAFKey); + if(res != null) + { + var appPlatform = App.Platform as MobilePlatform; + if(appPlatform != null) + appPlatform.virtualFilesystem = res; + App.Log("Openned dir"); + } + } + } +} diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MobilePlatform.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MobilePlatform.cs new file mode 100644 index 0000000..fd98eb4 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MobilePlatform.cs @@ -0,0 +1,92 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Platform.Storage; +using Newtonsoft.Json; +using Tesses.VirtualFilesystem; +using Tesses.VirtualFilesystem.Extensions; +using Tesses.VirtualFilesystem.Filesystems; +namespace Tesses.CMS.Avalonia.Android; + +internal class MobilePlatform : IPlatform +{ + string configpath; + MainActivity activity; + public const string MySAFKey = "MySafKey"; + public MobilePlatform(MainActivity activity) + { + this.activity = activity; + configpath=activity.FilesDir?.AbsolutePath ?? ""; + if(File.Exists(Path.Combine(configpath, "tesses_cms_app.json"))) + { + var conf=JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(configpath, "tesses_cms_app.json"))); + if(conf != null) + _conf = conf; + + + + } + var res= SAFFileSystem.GetSAFFromSharedStorage(activity,MySAFKey); + if(res != null) + virtualFilesystem = res; + } + + Configuration _conf = new Configuration(); + public Configuration Configuration => _conf; + + public bool PlatformUsesNormalPathsForDownload => false; + + internal IVirtualFilesystem? virtualFilesystem; + public IVirtualFilesystem? DownloadFilesystem => virtualFilesystem; + + + public async Task BrowseForDownloadDirectoryAsync() + { + /*var sp=App.Window?.StorageProvider; + if(sp != null) + { + var res=await sp.OpenFolderPickerAsync(new global::Avalonia.Platform.Storage.FolderPickerOpenOptions{Title="Browse for a downloads folder"}); + + if(res.Count > 0) + { + var first=res.First(); + string? path=first.TryGetLocalPath(); + if(!string.IsNullOrWhiteSpace(path)) + { + _conf.DownloadPath = path; + await WriteConfigurationAsync(); + } + } + }*/ + try{ + if(virtualFilesystem != null) + SAFFileSystem.Revoke(activity,MySAFKey); + }catch(Exception ex) + { + _=ex; + } + virtualFilesystem=null; + SAFFileSystem.RequestDirectory(activity,true,true); + await Task.CompletedTask; + + } + + public async Task ReadConfigurationAsync() + { + if(File.Exists(Path.Combine(configpath, "tesses_cms_app.json"))) + { + var conf=JsonConvert.DeserializeObject(await File.ReadAllTextAsync(Path.Combine(configpath, "tesses_cms_app.json"))); + if(conf != null) + _conf = conf; + } + } + + public async Task WriteConfigurationAsync() + { + + await File.WriteAllTextAsync(Path.Combine(configpath, "tesses_cms_app.json"),JsonConvert.SerializeObject(_conf)); + + + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Properties/AndroidManifest.xml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Properties/AndroidManifest.xml new file mode 100644 index 0000000..6080754 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Properties/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/AboutResources.txt b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/AboutResources.txt new file mode 100644 index 0000000..4cedede --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/AboutResources.txt @@ -0,0 +1,44 @@ +Images, layout descriptions, binary blobs and string dictionaries can be included +in your application as resource files. Various Android APIs are designed to +operate on the resource IDs instead of dealing with images, strings or binary blobs +directly. + +For example, a sample Android app that contains a user interface layout (main.axml), +an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png) +would keep its resources in the "Resources" directory of the application: + +Resources/ + drawable/ + icon.png + + layout/ + main.axml + + values/ + strings.xml + +In order to get the build system to recognize Android resources, set the build action to +"AndroidResource". The native Android APIs do not operate directly with filenames, but +instead operate on resource IDs. When you compile an Android application that uses resources, +the build system will package the resources for distribution and generate a class called "R" +(this is an Android convention) that contains the tokens for each one of the resources +included. For example, for the above Resources layout, this is what the R class would expose: + +public class R { + public class drawable { + public const int icon = 0x123; + } + + public class layout { + public const int main = 0x456; + } + + public class strings { + public const int first_string = 0xabc; + public const int second_string = 0xbcd; + } +} + +You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main +to reference the layout/main.axml file, or R.strings.first_string to reference the first +string in the dictionary file values/strings.xml. \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable-night-v31/avalonia_anim.xml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable-night-v31/avalonia_anim.xml new file mode 100644 index 0000000..1fef3ac --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable-night-v31/avalonia_anim.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable-v31/avalonia_anim.xml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable-v31/avalonia_anim.xml new file mode 100644 index 0000000..4784f80 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable-v31/avalonia_anim.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable/splash_screen.xml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable/splash_screen.xml new file mode 100644 index 0000000..4cebfe2 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/drawable/splash_screen.xml @@ -0,0 +1,13 @@ + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values-night/colors.xml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values-night/colors.xml new file mode 100644 index 0000000..0f6f6c8 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values-night/colors.xml @@ -0,0 +1,4 @@ + + + #212121 + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values-v31/styles.xml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values-v31/styles.xml new file mode 100644 index 0000000..8075ffa --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values-v31/styles.xml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values/colors.xml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values/colors.xml new file mode 100644 index 0000000..6fbae25 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values/colors.xml @@ -0,0 +1,4 @@ + + + #FFFFFF + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values/styles.xml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values/styles.xml new file mode 100644 index 0000000..77ed2d7 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Resources/values/styles.xml @@ -0,0 +1,12 @@ + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/SAFFileStream.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/SAFFileStream.cs new file mode 100644 index 0000000..36869a5 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/SAFFileStream.cs @@ -0,0 +1,531 @@ +using Android.Content; +using Tesses.VirtualFilesystem; +using Android.App; +using Android.Content.PM; +using AndroidX.DocumentFile.Provider; +using Tesses.VirtualFilesystem.Extensions; +using System.Collections.Generic; +using System; +using System.IO; +using Tesses.CMS.Avalonia; +namespace Tesses.VirtualFilesystem.Filesystems; + + +public class SAFFileSystem : SyncFileSystem +{ + public const int RequestCode = 1447775022; + public const string SharedPreferencesFile = "tesess_vfs"; + public static void RequestDirectory(Activity app,bool canwrite=true,bool presistant=false,int reqCode=RequestCode) + { + Intent intent = new Intent(Intent.ActionOpenDocumentTree); + intent.AddFlags(ActivityFlags.GrantReadUriPermission|(canwrite? ActivityFlags.GrantWriteUriPermission : 0)|(presistant?ActivityFlags.GrantPersistableUriPermission:0)); + app.StartActivityForResult(intent,reqCode); + } + + public static SAFFileSystem? GetSAFFromResponse(Context? app,Intent? intent) + { + if(app == null || intent == null) return null; + var uri=intent.Data; + if(uri != null) + { + return new SAFFileSystem(uri,app); + } + return null; + } + public static SAFFileSystem? GetSAFFromResponsePresistant(Context? app,Intent? intent,string key) + { + if(app == null || intent == null) return null; + var uri=intent.Data; + if(uri != null) + { + var r = app?.ContentResolver; + if(r == null) return null; + var rw = intent.Flags & (ActivityFlags.GrantReadUriPermission | ActivityFlags.GrantWriteUriPermission); + r.TakePersistableUriPermission(uri,rw); + var app2=app?.CreatePackageContext(app.PackageName,0); + var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private); + + sharedPrefs?.Edit()?.PutString(key,uri.ToString())?.Apply(); + + + return new SAFFileSystem(uri,app); + } + return null; + } + public static void Revoke(Context? app,string key) + { + var app2=app?.CreatePackageContext(app.PackageName,0); + var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private); + var res=sharedPrefs?.GetString(key,null); + var r = app?.ContentResolver; + if(r == null) return; + + if(string.IsNullOrWhiteSpace(res)) return; + var uri = Android.Net.Uri.Parse(res); + if(uri == null) return; + r.ReleasePersistableUriPermission(uri,ActivityFlags.GrantReadUriPermission); + sharedPrefs?.Edit()?.Remove(key)?.Apply(); + } + + public static SAFFileSystem? GetSAFFromSharedStorage(Context? app,string key) + { + if(app == null) return null; + var app2=app?.CreatePackageContext(app.PackageName,0); + var sharedPrefs=app2?.GetSharedPreferences(SharedPreferencesFile,FileCreationMode.Private); + var res=sharedPrefs?.GetString(key,null); + + if(string.IsNullOrWhiteSpace(res)) return null; + var uri = Android.Net.Uri.Parse(res); + if(uri == null) return null; + + return new SAFFileSystem(uri,app); + } + + + public SAFFileSystem(global::Android.Net.Uri uri,Context? ctx) + { + Uri = uri; + Context = ctx; + + } + public global::Android.Net.Uri Uri {get;set;} + public Context? Context {get;set;} + + + public override void CreateDirectory(UnixPath directory) + { + if(Context != null && Uri != null) + { + + var dir = DocumentFile.FromTreeUri(Context,Uri); + if(dir == null) return; + + foreach(var item in directory.Parts) + { + var dir2= dir?.FindFile(item); + if(dir2 != null) + { + if(!dir2.IsDirectory && !dir2.IsFile && !dir2.IsVirtual) + { + dir = dir?.CreateDirectory(item); + } + else if(dir2.IsDirectory) + { + dir = dir2; + } + } + else + { + dir = dir?.CreateDirectory(item); + } + } + + } + } + + public override void DeleteDirectory(UnixPath path) + { + if(Context != null && Uri != null) + { + + var dir = DocumentFile.FromTreeUri(Context,Uri); + if(dir == null) return; + + for(int i = 0;i EnumerateFileSystemEntries(UnixPath path) + { + if(Context != null && Uri != null) + { + + var dir = DocumentFile.FromTreeUri(Context,Uri); + if(dir == null) yield break; + + if(path.IsRoot) + { + foreach(var _item in dir.ListFiles()) + { + yield return path / _item.Name; + } + yield break; + } + + for(int i = 0;i "a" + //FileMode.Create -> "w" + //FileMode.CreateNew -> "w" + //FileMode.Open -> "r or w" + //FileMode.OpenOrCreate -> "r or w" + //FileMode.Truncate -> "t" + //"r", "w", "wt", "wa", "rw" or "rwt" + + Stream? strm = null; + + if(access == FileAccess.Read) + strm = Context?.ContentResolver?.OpenInputStream(uri) ?? null; + + if(access == FileAccess.Write) + if(mode == FileMode.Truncate) + strm = Context?.ContentResolver?.OpenOutputStream(uri,"wt"); + else if(mode == FileMode.Append) + strm = Context?.ContentResolver?.OpenOutputStream(uri,"wa"); + else + strm = Context?.ContentResolver?.OpenOutputStream(uri,"w"); + + if(access == FileAccess.ReadWrite) + if(mode == FileMode.Truncate) + strm = Context?.ContentResolver?.OpenOutputStream(uri,"rwt"); + else + strm = Context?.ContentResolver?.OpenOutputStream(uri,"rw"); + + + + + + if(strm == null) throw new IOException("Failed to open stream"); + return strm; + + } + + public override void SetCreationTime(UnixPath path, DateTime time) + { + + } + + public override void SetLastAccessTime(UnixPath path, DateTime time) + { + + } + + public override void SetLastWriteTime(UnixPath path, DateTime time) + { + + } +} diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Tesses.CMS.Avalonia.Android.csproj b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Tesses.CMS.Avalonia.Android.csproj new file mode 100644 index 0000000..d3a34d9 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Tesses.CMS.Avalonia.Android.csproj @@ -0,0 +1,29 @@ + + + Exe + net8.0-android + 21 + enable + com.CompanyName.Tesses.CMS.Avalonia + 1 + 1.0 + apk + False + + + + + Resources\drawable\Icon.png + + + + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/DesktopPlatform.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/DesktopPlatform.cs new file mode 100644 index 0000000..7088291 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/DesktopPlatform.cs @@ -0,0 +1,95 @@ +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Platform.Storage; +using LibVLCSharp.Avalonia; +using LibVLCSharp.Shared; +using Newtonsoft.Json; +using Tesses.VirtualFilesystem; +using Tesses.VirtualFilesystem.Extensions; +using Tesses.VirtualFilesystem.Filesystems; +namespace Tesses.CMS.Avalonia.Desktop; + +internal class DesktopPlatform : IPlatform +{ + public DesktopPlatform() + { + + if(rootfs.FileExists(Special.LocalAppData / "tesses_cms_app.json")) + { + var conf=JsonConvert.DeserializeObject(rootfs.ReadAllText(Special.LocalAppData/"tesses_cms_app.json")); + if(conf != null) + _conf = conf; + + var path=UnixPath.FromLocal(_conf.DownloadPath); + virtualFilesystem = rootfs.GetSubdirFilesystem(path); + } + } + LocalFileSystem rootfs=new LocalFileSystem(); + Configuration _conf = new Configuration(); + public Configuration Configuration => _conf; + + public bool PlatformUsesNormalPathsForDownload => true; + + IVirtualFilesystem? virtualFilesystem; + public IVirtualFilesystem? DownloadFilesystem => virtualFilesystem; + + + public async Task BrowseForDownloadDirectoryAsync() + { + var sp=App.Window?.StorageProvider; + if(sp != null) + { + var res=await sp.OpenFolderPickerAsync(new global::Avalonia.Platform.Storage.FolderPickerOpenOptions{Title="Browse for a downloads folder"}); + + if(res.Count > 0) + { + var first=res.First(); + string? path=first.TryGetLocalPath(); + if(!string.IsNullOrWhiteSpace(path)) + { + _conf.DownloadPath = path; + await WriteConfigurationAsync(); + } + } + } + } + + public async Task ReadConfigurationAsync() + { + if(await rootfs.FileExistsAsync(Special.LocalAppData / "tesses_cms_app.json")) + { + var conf=JsonConvert.DeserializeObject(await rootfs.ReadAllTextAsync(Special.LocalAppData/"tesses_cms_app.json")); + if(conf != null) + _conf = conf; + } + } + + public async Task WriteConfigurationAsync() + { + await rootfs.WriteAllTextAsync(Special.LocalAppData / "tesses_cms_app.json",JsonConvert.SerializeObject(_conf)); + var path=UnixPath.FromLocal(_conf.DownloadPath); + virtualFilesystem = rootfs.GetSubdirFilesystem(path); + } + + public Control CreatePlayer() + { + return new VideoPlayer(); + } + + public void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer) + { + var ctrl = control as VideoPlayer; + if(ctrl != null) + ctrl.MediaPlayer = mediaPlayer; + } + + public MediaPlayer? GetMediaPlayer(Control control) + { + var ctrl = control as VideoPlayer; + if(ctrl != null) + return ctrl.MediaPlayer; + return null; + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Program.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Program.cs new file mode 100644 index 0000000..d489fe8 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Program.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia; +using Avalonia.ReactiveUI; + +namespace Tesses.CMS.Avalonia.Desktop; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) { + App.Platform = new DesktopPlatform(); + + + BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + } + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace() + .UseReactiveUI(); +} diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Tesses.CMS.Avalonia.Desktop.csproj b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Tesses.CMS.Avalonia.Desktop.csproj new file mode 100644 index 0000000..44b97dc --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/Tesses.CMS.Avalonia.Desktop.csproj @@ -0,0 +1,28 @@ + + + WinExe + + net8.0 + enable + true + + + + app.manifest + + + + + + + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/VideoPlayer.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/VideoPlayer.cs new file mode 100644 index 0000000..5de7a5b --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/VideoPlayer.cs @@ -0,0 +1,124 @@ +using System; +using System.IO; +using Avalonia.Controls; +using Avalonia.Threading; +using LibVLCSharp.Avalonia; +using LibVLCSharp.Shared; +using Tesses.CMS.Avalonia; + +public class VideoPlayer : Grid +{ + public VideoPlayer() + { + view=new VideoView(); + + + + this.RowDefinitions.Add(new RowDefinition(GridLength.Star)); + this.RowDefinitions.Add(new RowDefinition(GridLength.Auto)); + this.Children.Add(view); + SetRow(view,0); + Grid seekPanel = new Grid(); + Button ss=new Button(); + ss.Content = "SS"; + ss.Click += (sender,e)=>{ + //take screenshot + string realDir=App.Platform.DownloadFilesystem == null ? Environment.GetFolderPath(Environment.SpecialFolder.MyPictures) : Path.Combine(App.Platform.Configuration.DownloadPath,"Screenshots"); + Directory.CreateDirectory(realDir); + if(MediaPlayer == null) return; + string name = Path.Combine(realDir,$"TessesCMS-Screenshot-{DateTime.Now.ToString("yyyyMMdd_HHmmss")}-{TimeSpan.FromMilliseconds(MediaPlayer.Position*MediaPlayer.Length).ToString().Replace(":","_")}.png"); + MediaPlayer.TakeSnapshot(0,name,0,0); + + }; + slider = new Slider(); + slider.Minimum = 0; + slider.Maximum = 1000000; + + slider.ValueChanged+= (sender,e)=>{ + if(MediaPlayer != null) + { + if(!theyareseeking) + MediaPlayer.Position = (float)(slider.Value/slider.Maximum); + } + }; + playBtn=new Button(); + playBtn.Content = "Play"; + playBtn.Click += (sender,e)=>{ + if(MediaPlayer != null) + { + if(MediaPlayer.IsPlaying) + { + + MediaPlayer.Pause(); + } + else + { + MediaPlayer.Play(); + } + } + }; + seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star)); + seekPanel.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto)); + seekPanel.Children.Add(playBtn); + seekPanel.Children.Add(slider); + seekPanel.Children.Add(ss); + SetColumn(playBtn,0); + SetColumn(slider,1); + SetColumn(ss,2); + this.Children.Add(seekPanel); + SetRow(seekPanel,1); + } + Slider slider; + Button playBtn; + VideoView view; + public MediaPlayer? MediaPlayer + { + get=>view.MediaPlayer; + set{ + if(view.MediaPlayer != null) + { + view.MediaPlayer.Paused -= Paused; + view.MediaPlayer.Playing -= Playing; + view.MediaPlayer.PositionChanged -= PositionChanged; + } + view.MediaPlayer=value; + if(view.MediaPlayer != null) + { + view.MediaPlayer.Paused += Paused; + view.MediaPlayer.Playing += Playing; + view.MediaPlayer.PositionChanged += PositionChanged; + } + } + } + + + + bool theyareseeking=false; + + + private void PositionChanged(object? sender, MediaPlayerPositionChangedEventArgs e) + { +Dispatcher.UIThread.Invoke(()=>{ + theyareseeking=true; + slider.Value = (e.Position*slider.Maximum); + theyareseeking=false; +}); + } + + + + private void Playing(object? sender, EventArgs e) + { + Dispatcher.UIThread.Invoke(()=>{ + playBtn.Content="Pause"; + }); + } + + private void Paused(object? sender, EventArgs e) + { + Dispatcher.UIThread.Invoke(()=>{ + playBtn.Content="Play"; + }); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/app.manifest b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/app.manifest new file mode 100644 index 0000000..3487fae --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Desktop/app.manifest @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.sln b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.sln new file mode 100644 index 0000000..4690536 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.sln @@ -0,0 +1,50 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32811.315 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Avalonia", "Tesses.CMS.Avalonia\Tesses.CMS.Avalonia.csproj", "{EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Avalonia.Desktop", "Tesses.CMS.Avalonia.Desktop\Tesses.CMS.Avalonia.Desktop.csproj", "{ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tesses.CMS.Avalonia.Android", "Tesses.CMS.Avalonia.Android\Tesses.CMS.Avalonia.Android.csproj", "{7AD1DAC8-7FBE-49D5-8614-7321233DB82E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3DA99C4E-89E3-4049-9C22-0A7EC60D83D8}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBFA8512-1EA5-4D8C-B4AC-AB5B48A6D568}.Release|Any CPU.Build.0 = Release|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ABC31E74-02FF-46EB-B3B2-4E6AE43B456C}.Release|Any CPU.Build.0 = Release|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1C1A049E-235C-4CD0-B6FA-D53AC418F4DA}.Release|Any CPU.Build.0 = Release|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBD9022F-BC83-4846-9A11-6F7F3772DC64}.Release|Any CPU.Build.0 = Release|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AD1DAC8-7FBE-49D5-8614-7321233DB82E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {83CB65B8-011F-4ED7-BCD3-A6CFA935EF7E} + EndGlobalSection +EndGlobal diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml new file mode 100644 index 0000000..649c230 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml.cs new file mode 100644 index 0000000..c551c12 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/App.axaml.cs @@ -0,0 +1,160 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Avalonia.Media; +using Avalonia.Platform.Storage; +using Tesses.CMS.Avalonia.ViewModels; +using Tesses.CMS.Avalonia.Views; +using Tesses.CMS.Client; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Tesses.VirtualFilesystem; +using Tesses.VirtualFilesystem.Extensions; +using System.IO; +namespace Tesses.CMS.Avalonia; + +public partial class App : Application +{ + public static IPlatform Platform {get;set;} = new NullPlatform(); + + public static MainWindow? Window {get;set;} + + public static TessesCMSClient Client {get;} = new TessesCMSClient(); + + public override void Initialize() + { + + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + Client.RootUrl = Platform.Configuration.ServerUrl; + string title = GetApplicationTitle(); + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + Window = new MainWindow + { + DataContext = new MainViewModel(title), + Title = title + }; + desktop.MainWindow = Window; + } + else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewPlatform) + { + + singleViewPlatform.MainView = new MainView + { + DataContext = new MainViewModel(title) + }; + } + + base.OnFrameworkInitializationCompleted(); + } + + internal static string GetApplicationTitle() + { + string title="Tesses CMS"; + Task.Run(async()=>{ + try { + var data = await Client.GetBrandingAsync(); + title = data.Title; + }catch(Exception ex) + { + _=ex; + } + }).Wait(); + return title; + } + + internal static async Task GetMovieThumbnailAsync(string username, string name) + { + //we need to cache the resource + if(Platform.Configuration.CacheResources && Platform.DownloadFilesystem != null) + { + UnixPath cacheDir = Special.Root / "Metadata" / "Cache" / username / "Movies" / name; + Log($"About to create directory: {cacheDir}"); + + await Platform.DownloadFilesystem.CreateDirectoryAsync(cacheDir); + Log($"Created directory: {cacheDir}"); + UnixPath thumbnailPath = cacheDir / "thumbnail.jpg"; + + if(await Platform.DownloadFilesystem.FileExistsAsync(thumbnailPath)) + { + Log($"Image exists: {thumbnailPath}"); + using(var sr = await Platform.DownloadFilesystem.OpenReadAsync(thumbnailPath)) + { + MemoryStream ms = new MemoryStream(); + sr.CopyTo(ms); + ms.Position=0; + Log($"Image read from file: {thumbnailPath}"); + return new Bitmap(ms); + } + + } + else + { + var metadata = await Client.Movies.GetMovieContentMetadataAsync(username,name); + if(metadata.HasThumbnail) + { + using(var strm = await Platform.DownloadFilesystem.OpenAsync(thumbnailPath,System.IO.FileMode.Create,System.IO.FileAccess.Write,System.IO.FileShare.None)) + { + MemoryStream ms=new MemoryStream(); + Log($"About to read from network and save: {thumbnailPath}"); + + await Client.Movies.DownloadThumbnailAsync(username,name,ms); + ms.Position=0; + ms.CopyTo(strm); + ms.Position=0; + Log($"Image read from network and saved: {thumbnailPath}"); + return new Bitmap(ms); + } + } + + } + } + else + { + var metadata = await Client.Movies.GetMovieContentMetadataAsync(username,name); + if(metadata.HasThumbnail) + { + MemoryStream ms=new MemoryStream(); + Log($"About to read from network: {username} {name}"); + + await Client.Movies.DownloadThumbnailAsync(username,name,ms); + ms.Position = 0; + Log($"Image read from network: {username} {name}"); + + return new Bitmap(ms); + + } + + } + Log($"Image does not exist: {username} {name}"); + + return new Bitmap(AssetLoader.Open(new Uri("avares://Tesses.CMS.Avalonia/Assets/no-media-icon.png"))); + } + + public static void Log(string text) + { + lock(App.Platform) + { + var fs=App.Platform.DownloadFilesystem; + if(fs != null && App.Platform.Configuration.Log) + { + + using(var strm = fs.Open(Special.Root/"Logs.txt",FileMode.Append,FileAccess.Write,FileShare.None)) + { + using(var sw = new StreamWriter(strm)) + { + sw.WriteLine(text); + } + } + } + } + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/avalonia-logo.ico b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/avalonia-logo.ico new file mode 100644 index 0000000000000000000000000000000000000000..da8d49ff9b94e52778f5324a1b87dd443a698b57 GIT binary patch literal 176111 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 zds⪻d~AI?xBz^Z$N&azD%i`bv67=(oXHrTmj{nwVMNAhEzCHOseHxL&sUcX=NAare!0~<0z-2WVQ3ehVF)HLE-`4mXn(Av@p6l+tNjk~xtji-(e zPN6*QB6Y-CwMKF5<9<(q_Y)>e7$NJp8A#d~syTIOO2JP;AMFnk@GYhvVoJ-w(6*)~ z5BImC&YPxQyU*k`Y|1hoN?DiaGv-r_^ zU7i!Y{85Y7R zv-910u%)t|tWg@TfQK3+q{eaw7gL}k!00&5YMvl(5F_KEih_^2y8<@$2J*if0MS<6|_rW*im{p>@-kL(z}@NyzzN; zDiX8!Iz9MJ7a*~OJ{t_P`^g@^i?E)5kJr$xV(=ksawzsJ6qqZdrpG6mvXcQfjZ1^m z>`b7$WME43w?6qiH~B~Pbd=-~eUyaZCH6Exkd!W@m+p$Ty^#g%B}TgxH)w0VGx38O zrsYlPB^N0GD;jWK|0ZYDerGUNyU{Rx)r}7%E3B@>X=i8QR-njnPkl^XgbkIQ0~bL* z&j+0518@Eo>hhuW8~bIlH_}JWh9-~aLkemRrt!M1#2}82;zdI}>PE#_wWo*qI@nMq zwOGAqYX0K+e?sF<;~qK(m_tw$qAIu1aI7@(A(Jn@XbkJtn*^}Ch)@jN0lQdUWJ82J0mS+nJhjCxLGBgxG1Ghc8S zjjEmCnT_CTaiv8>KD3>d$Qzq`e<4EP{jKpa?nq<6cEtop%*yC!x3|($?O7G!}g)k3LfgsQ@a=38U+W0u{{S;Ksr3`hXH>@}|mbxiS zw`pc0qao)`G5$MB6I&f>vwGLrLe3{qa8K?~I|gC_;jm6v?pdL(LN<#{p9b^38?A~! z6~C@5bu^lngtN2hVFwx7mQS0DJLyTK8j3M5^8hmFq*wLN;h_cK_QvyGtE}SmYH*+y zQc}QAf2m|OsA&DE2Yige6#>XL+3)37`uU<*Gtz0>_@W%dwlx~C8fj@4xHJ6s)BL=h z7CiQ8>j`!q=6lAvN|-8Yx`;6?$vU4Ik!Obvhu(ar^LVgM`@b0s9cnGJei~vypd6oB z>z?7bS(D-}>TtLKIl+}iWh!e%m-m8rx>7rNP0ZCM`WQiLGXK*PO3>5%skCPHGfGc3 zbdhxn@O=&fb#8bLJUu^;0<&0bWWH36|BGrlM~o5NQ%7xP9fuo-y|c9IDRZ-gKkeS` ztlqRQr>n4bd6Gg3au*bq5r1g(={CQ|wzLa>kyRavuNfRS>N@hGtpE4My&WF z9kFQp6$J-i%es_dJ1SixKlqfg0ZJ+@x%K6kgUtV3^CG7%-o583!4!^D5p}J!thTu% zM*w~W4{)sr{%azA(TK3L&;RJSDmbGuOQuRmS}L7Jl+>vlgjiafJ`S{9)LehFYd4gZ zs|kv(wX=L9kI`&s%hF%Eu6kj4Ct}gifoFgh`3FR*=+P$1?-dQ|%!Y+s^5*=Fa||LH zza2mP+0{H`U5gRQTDylE83AoztVsB#o^DBBb7p@(`1Xy5p&}k#+Uu)ypNHt_k1s84 z0QgbWR<0qh?~c1h=a_R-`M_-JYs@YIxEltH{fMRK#bSO~tXJ#V#n9-l4dr)&Z*#a5 zWfZ;ar4i_+`(+h}j`)iQ>QkS;DzyUe>2}Yr!F=SMg*0 zr@F}iMske>UJfqW+ItP?E+?9K$+_CZVYe7>{Ci4Nt8BDOOC33^3{9-Xm+`f&B)`-cyv2XBX zu$f{z{_15I~xOXCInL;V~64JV2aA{x23*;UpuADLwN z7(S7q$;6uM=tIbB>r@@^Z8c9>W(*?2vi5e;KY;l~u4`6|N<4i(RcRR_I76(e>@iXg z%#*HWKpWDZ_%QdZJ7){9&K(Cbe_)2~8r1J|W(z6;KL$`1|y zf6s4*ZL4ihvUUcYTLQc~RkRPALu~w(Ix$K^g7TLXtyQq~DyKZ6a}&5rbwvU&3R6E_ za97C1QHyh?c{zvWyJDsK@FF}Wtt!_hovTj9#a{B|&Zn!bxQxcLp>Zw%iKvG5sTjlT zrReO7pn+3U%Dbqz-R=ZtJT4Dwd?ZxozVEsM#pMTEfC=EzfYPBj>-LlqEpUhjXFq<6 zWd;7GSxt=4RLF0>HHgyJ=eb$3ThN%hc-xD;@^~>YzI>$8CG+F(pPYyQ`yTpqrp9d~ zyNtWr<0PX{*??kjDDFW(UyE&|&N%wjlIhj4czrTBd$DZXTKrVJ{nCuiQbtDbcbJ7! z3PuX=7y6i@Aa@m9pC*ynL-_oc4{p@iJOYEjHeCPP#>Ma4z>6)oD6@Oom+xhy=v(~b7iixbvsZ5B>Xi|aa%7CS`V9R) z)lUhH$WmT5U+O=D(^Ke3pqV?J3D3W0#l{#l6`d@qnQBXj~Q1c-jJL zOAHcBeTwoUuRp=z6)1+|NVH9cHH2H>d1rMwJ0|xxy;b50dg-|}#(KD=>jL=6VW4!g zlJG%|+^s3TDI&UgA-7Y47{!2|JFF=TB_2g4{nb~u;MB(~ZQOz~DNq#etww&?cZL2` z&&s}9j#%b6RD31vgIKeKU9IZolji;Hq|HgM0ggbzZ58Oh#1ZO+;rnt}Bwy}OU);dr zA=;*@Z%K>ueOjVKi!@WdLcI*0R#e!yFpS*Dp)9mQg#(0v0VmX?33HE0e!e&)l%IFj|YvrG-nGw*drl2Z^+=5$z+v1w!sfE~G0m(MMh|gZ&hJj><(xr7(Jw zxLv;w^b9-_>#-XYqCsa417|8-5d)n;|FcBY%Z&j*Y&!vRqScG8!KMAe$><*YLA+ig z?4T@1bN1Rqg&5I(39IX$mH5#y%#MIPV8~6XLB>vxm&VT8KhLXV)Oz)MmCf?Dh%L8% zA{&a96nf7KWc3Y%K-uCHw&?`NOdw&+8<&=$-`!xo8K7`)2 zxCz5itd!JrmN&AAZyIKFf?#8~SpQ&dY)Taes%`r)KXY5BCXD;;=W7u_E&xO3`%cUo zKJc~Rv@@{_3UC*t@R?Q@ z%+YPku=b`qf_AQny7~5V(&n8Kjorxw-A3wo$p3xo^#=+yN~Q0%Blb#LlHLjQ7^x*) z*hrOCn~;>@gj`niwv>P@PWVY*#;0{1tX*j(*~mV9SprlABElX$9x1(6E>Z57Kv500 z+s9$@%ZSG#z#^a1bZFCL!Im0q&!pkx@-1(kPJ-XV{1<}FlpJQj-kXhPbz^9(dh33DH_)Fb1y^F*5q5xPrJob4wR?Jx@9w)^ zwW)=gX7A@)SVdU6VF1LK%JbXhCLYXUfPgCoQzR#)#TEptT-xbo6^06%LUXRlby;ykz>VhMaAdd!% zdS5j*vG7`7aCU|F-D$Bwkp%-f2pVt$Lx0wZJp3##$A~cB1B(}bh2*go1ta_*1lxym z-`9S+qT6_1MZg2zH2jw< z3S_~q3xuB>q<}p2^-A+LDqi5Wt&#(iazPEHj6j)UfwV!zpfgg`{Jm z!SlmD%n6n<@INoI9Qi|YI;s9UY{(vs$q<23TEaT`uJhM%@D_Vspt~6K>XFY4!n)uH z##J{p>MhBBa9^zWSr=gRwlJ~p@Yqhu;8!`5qP|aGCJs4(umi6i{=45O$H6=FrU94y z(^+cNPCnCbdf@W?5XJs7g$Sg{b}N_sHfZy753v6j0l~G`s-P9LvrKJnTxt13`)8#! zp~-?2NGZ%u>jn|(wGj(C{+>}HAHCH_sFQ9{*Z;I9o;|8zj*mpXyR|!dd}90IzTZqZ zdd6fJ;W?WROz6zQg)2U)ALxQv9n@p9;HS0zJ&+o=?_hl?=A|mv?^?}8)wDOb}VgLcVW_*T1LPEMZUpCk0T7e9EXmPn8gEjUj2acD^B=Ttv1?;I^WNh z{aKOk55sN1m0VU0(qx%AbwACkyzPsWOoEGjakP$B4h^Ad&o6xJp59Mw?bomOx?ll5K|UIQaov0p_+#63rQKdIts8_@`?=|4 zK~?XjgtLlzb+ODjXC$g$-qZk?{gdKZ?5EX{(&~fqw~{C?*qox{dLDCPN+JN)UE^eI z%C<;FT+R&R$IF$|IZbCUS9P<$ijv5M8S^l5z>gG<6Cpm!)oYP&D#h1JS0S3eF8je+ zH2BW`Jz2I)4qV?jLp=Xv{2;L zdc_M+ag-RP$lOkgT2DLTYNHAKhfu-fODl3Z-X0qJ5uWO0G(`!#l9rA1PLP(MvW80d z8&MDB$2>u;nZTR(Y}dhrsz>kY+p?@iOqxSaZ^i@cBwWTrPIMUwv`vk=T0^dU?lO}) z$o1Bm-pTm9*lBezC;fPSNVr>sJxck_3h`Dhz%WP1_sL$r9AAy4P3cA_tA6GZVRlY! zl3i9;>JMPd)NVYb)^m=ivqB=^l5To~Xl_2xHM%9H6)(vcre9n&_olY6H}*>oZ@{&t zy#d3@!oEtEbGnfsF4i8;+5;Njh8gV4cPooxTg z`dvR;i+Phr4vrl!J`#lvEElRYC{bGGE2fGbV@hYvp`_Ug$f;mozc!?m~F3IM@RlTi! zu@|dEGKGjco`6I18zWuRj(s@OK7f=WkR&!(wN6r`a&76VQq+9mJlh#=#M(354 z)PX!gY~$xY4~!IsJAGkkZy@gW$5{A^9J{F4^>i|lhFrv!`q)htqCYJeyesiepO*(d z*E{;Zro?)mpK=0X%K7QdO!j@M$3Q@#bT;c)(3&$woWBVAZxS8yyz7E)T4oGRKg$7HGVDRM z1Fih{kxo}Og$Qwxy-Dx+@bD&vA{Ux2b zhH_U{Md101Sg23{ulJ~ji&~L}?738D5ZoFt;r9I=J?4pl@b};M)?J8}+vY4zywc9m zLmA>sa;o-Qz>4J80U&gnx>ulF5}2IuFMqvFcEV_shUr%>@l(vsTS09hpO(x#g#Q+Y zWtN53ObP_xPO9V9`aA}#yp)O^5Gp$0%){<>@21v+SsJdTSe3>;o2{bp4Y-YZIS^IM zL33=d;aDEDUyEm8^0W4P+h2W|BZYwejKI3Z#WSG=1u`FUz&W$JH73V5dJtBxyR~#g z_o>_>J}^25xF&Fd_i5*BZExT)dvy25U#o#ASDBAjJOpXUpJ;FwUAx(DD1IpB*m*Ne>#<8a=HmI|(Evlq|xxr6}G+_3~ z$=Wrwp)}Wy^=CtuU;2Ffjyl_?)Rc_q{<@v?sy$y+iQ?4v-dT4={<09&$9%5?FQt3P zbqy-Fn}xG?e%)0_3=MdO^=R*r*yW&bq)aO^^chmpq4p#rKNc60boH18@3jH>it&Q& zwsMzI=&9_=4O?~gwy;_~P#OxmeL8fB9y$4ua_PAXP~BfRHU8ybl!J>t%+}(T(lHPr z{O_rT9D7%D2=+vA7NBn-M=N2TZkfrRdB48s^Tz5su*c)tb+_fe%p)f|zAx>3+;_~fTV7s_r@K(Nh3i$3jJ$6U zcVK7Q*;jnvn$RS#6o5$4gjMG2a` z?2uFiedNQ*Jy8;It1A$J3-ZR~0#$4r2kkwh5hr9?F-3!t2$9(3cUGTcAnnDoo2c&` z(3(IvzgG-cju30KkOM;3tnJ_l&!RvpY_XTenk$qnCC}>m-2d$b0QvwEEdtt>oQYX5 zxia}F{z@%CrI~0@dFV+=s(t30Lz|@+z@t7%d3xFCt<(O0-HuDyF&IW6ykD?wa&Qo; z^qn+t;62aCQwfjXV`u0InW6AV>a3pcrc0+sQ4MbG z;8%sn{m$(uFo^^a5-HJmZFC8qAt~*K@+IrU?ivmJu{aH|*AuU%*REhsm%i26!zi* zLUeae>*$myRB&N>KqZ>DnkEGKRgd5t86 zx+KW{O5`94unfma(D8B`Z@bMXiRi-8lr!wsAm`6 z>Ajv<>9m`B{!eOsKZtut#4eV9ge}-5{-7~hZzIraUreVocvUh`!GAgW55&U#2REe0TET=wH%!J*exZ_!abCvQhh+W_=jE!EuOlRvM#7J_^u(Ss((C4PPzkJ`v zQU@nDHON-Q!j+Cb+N}Wuh|TiTC$m=kKAjeU>JtZ~wac(0^@Er!mLitL=XM$Z$(Qs+ z0YqQP3lLNpShl{IV%Mt2rn}%!u7(NOyStP6R1a%a5nc6qGY->pba#)&;xUlg=YyKX zO@nqo8>+rF^{=;q@UOHjDBH-@G|t6sg?)fW;S&Ez`{xVmr#TXn#U4FI5bWWg_Q=%SvzyCTGu z>v){ zy}Lk9K)clyo0-DPXnUgs;={4*-qWk;5qoa;-S{E;%;s?vSRKW=zo#GYZ&AB7*q05h&ArdxktNXSx@&3>3Ui~DKlQbQx)Q;b!XJ>UUEFG%Zlv3kfP3*9Zd#tRdTtE% z%Gq}C)kpsETmf|D7FGT0yHCAXoTI-mW-k01r=Oy!sISkN8#B&5Z=8TuoYBPhEmxKp z^wY`~9XhTfWet&{n>;S)rB&tl{PANnjv2@vrRBI>=dQ2w&Y|s!&^6-QL;g}Wl&h2- zckQXZCU0ARLeZi}rM>xBr-R?2$bEcsa;D>Tm!!E2f}ARcd|fSl^czF{OwPVO3&-Ee z(8xW|D~wZWb8^S(emWiISOe)lTA<7n&SAoBQ%ij!x2MvYek9R{nFC`J7|{PC{+y!M ze(mX9bCj{oKcTpFeYU>jjN;Pw3!INsi*>zumC2u#@}}bQz)!vlJ;9-r zN-xtw2r9`y=fXqFyE$r;jDp62BA}@l!v|8?`d*>Cnf`K_zww998M2`QLfKA$wQ-11 zs~y!p{9&zp28&59(qoA30&XZa!8!9+EIFFdh$2IiMM9CWcn>KipEaX-j~%$T?_)Bv zbt)r_h1~IsTmhgKJY%3`QZ+Tts>`wPPN^`&?V#@qoP0{|V8vmMnlxg}z zVXkoIA>eF^Ib?m4KeB&D=G4fn)kHv*R9aT0;F%YB+Oq!!=I5T1+q`3Ep!piVH5yZE zEvamo^1XNG$c>kndc%aG8U5_;-bkXsQ>AO{>>|c2&&$9!7&CzNR!TCVKvw~1KK+Lq z5}V?k4Lq(mTmp@51>Nmlox5JYV8)H^pD;ucDEJkYE2!TSyja;^hLa~jROwwHGzINL z6HeGv`O!%t_QK?bSbM456-jY0kW~@XJnu!t*YOw&fP)nAHXiU0N=4F&lGGTPmgx$S z4}jaMD)XAf0t>0Db>xS3E*0q9lR@((Vdoa8yw}N$jG6}B@0D|CsHi!@uDL+tmX_8y za+owwlNZjB$Y!aKT2G)|^5uNrm$k7rqChpT6X)nO}Zo zvujmPJR=5gjIc})78lDysBAr|^VHY`=&X3EdIVQQn&WD_D8;2-LE+A|xOhOU`O_)Z zu`(z^1CpbXvAsk(j?zX|@@X~;&%eRP0S5@e&+V%VuobF!bhWlE|J^kV=VnKq>VRuH z=fE+1{r0kpBl`C1=bB%4lkgo^nFD&^D9CXHMy>{YBX)oNKpOEbZYEJ@!4E9}5$ccV zo5T%Bl14EWLlyh8eUEVk$J8qp7Lc zX80zG$6`Lqy8|M`j%UkdAVysGpiN0L5X~RZ+Ryq?HE=6>wbG5GugCu(unnw_JZg-E zqY(4Pi#c;d`Y`-+Wxt0~^DDIqaYL_fzZ* zI1tTY^-zOqY%4R9rB__@3{_EEW=re&Os9G3p8>ZLZ1?;U?fv%sTh%&!SAISEib*)iP$x?U37PVBo=|$aof2iWEKR^YLp`v z{Wa~s-~ZmM@2%()g0dQOP#fo{!OF|y8FW;b{6`VWL1!VFiIZMD<~1{vW%+r*hlc+Y z7fdLP6}cNPpX1d%S~(q_N)YP_-i*xLc|-^=C$R-J!4c&~D`YAtdMC(ezDqpvB2v%~ zUk<36bql_ccn#H~pqBH~$VJq#FbtRh>@?MF6B z^Fsouoy*L&Kt79`K6>F|WVdj7_eJ{}h#B7I>tLd^gC&722Hbap?q54ZfFCjyjeue# zkQa7@mjs>*8G=bzReNS}7kLJ5HHY*eP$y?5S-{`CqC zQwzF7lb?yeUtf6XAe{{D%lvt8VhyPd*)B0yzVaJ?%Zl_Ly#<6#G;*c_i;G-*ie7mc zy#hGn(X>hiDIXJ-xb&_ z&^C!MF0gVRbG9)Na&PrbcdrwN5N-Qy$n}`Zj}-Tb2OPoqhG+Ve(lF;B6}Nw0u}F?C zo8?Qf#U5FBM)ZQf+cHbrK5lFxL5`4cP)<1ld*>0Kp1k(y}mf7$LjXDJ1XRqja`73=gi63Pe;r(=&{9X*Ju^q_#&&Z zzG?Gfv@bFnPaEg*~jb$bCyljwX#!S$SBK$U zS`FV(7A_6lK*H&=^A8YIk&b|S$#X<=3w96XH_FPD3R+pT!fABE76T6U%$>@fpm(EB zKkCsGQ9OvS>gcE~JylcojHQ+g5S8PVx1_{wJKdkoFOQ$pF1m3aFaDjg_2QE!Bv!<~ z?cs!w_0v);ow&oV2d78d_NIX=&pUhwVgGM?{Mn&56p7GZ)p z<`!$d|A)Fdc;+~+s393eKd_Nv7#IP6&r{+Q+4-t|AH(#2+G}14A{LqIgO=x3^k8#7Qr^p*hK_7gP_@K zK0K^dQhEM%#F^0Rn#;}5i6{CRV06e5kas!IMP_k{{3JK0&i)!>o#L^VF~gi21G0~O z8|m;US+kD(3B=CLD~+rB8F^lgnHwAX+o`ePIwk7q^i8)S<&CsK8V9zAEOEWMlF@Tgjw|JQ8|LVm=j(PNVSewS zeQez7(4*ASpYsMu2IF*;g9dnkOLR!HGw?t80^n+~N@Sm>htZ%C$jRK;!<^&g4D;a* zKMUFj{GCd6gcQnRZD#SRYH; zdS(>mW3<<3ChmBNW?nTBqYkRK{jqwz9r(9^PlbvmwYnnnE5srl^sXSZ17_d7tCpYh zY2y2#MuuJRct4*Kmh0UwO<2S}w>Z2>ZCJ!HB*Ed7-WcVmAK~{R)=T|}USeP9)Nro= zT?Y5!_Q2zH@z9}Ds>INbAzv?oSPY_ONugZ!73zjMT?pi7oP1!7sr67h4CsO#&!~`b&5UM3WjK4s{ZMV}Iuf zd7GF6ls#lDCT5WKeN1nEPLEd1v4iO*C!1(LsO8zn>azofJpFa(iGcPBwDgAz%nUDx z5B{h<2tT9!EuK#d;vRc=$RG&uWuKluA(&vs$N*r}6+ZdlE_j)%m zG6jE^-BhY^cQ)NE?*tf~HTCwN)mG&-2p@3_NDB;S;VZk(|2`2SIOc#4#I7!X=XAcJ zlyjv+oirp{^KDziAwb_JOMK1mF|?21A@XX8kxo2{Md(!}$dmyp zHgeq=s#m^nT4IYY`y4{o6+~sp&J``*D?KS*n8B#4F#)M*=NAAeNT!sO3O>s?Sk+|$ z^Cq_NukM3*c{AFzO!Mf(wUt7yg-1S4S%s)PMz9MWz+hv9NP3vhUe95XGySu3Zg=FK zX*ow}fe%Q~Ki+{tFT+8vx(+`EIAwqUpO*0cE3KQ=NF?*M6bKEPL2QtwF}D%)Gjg5E z&fva<_AS(YX3Z*O$@Jmwu4ROZEfn{o4a@!P6|+ zZ3jltPn1Y+^#r{4cFU%`5QRK{CnK8A+T@O<)qWe@ztW*&^m@0+cKhe|o@O@mPeT&O zk3=f1VFf_#uPp#30$fii)CiP?IRq!5jRi=}>SU)PR(N$Dytx^ zE1&`iK@76SZGUN1qcD;nuGQ56_nFY4PX?Tt!g>DcxsGRFH~RjWnl=yks8{k@ApX{Z z>F&~UwpbP|MS=a^m~AaJ7z?2o(>315Gy91M>p9oc(A^KVn+q>r9^wc%z^Oy!HR5{? z$g>O-Du&3%b~~e_Nc_|QxoEdKUfINox`iNO%(qkH$p`HLK{c!ey&9K*3kq@v26R?^ zw_!Pfc2kZe7e2TB~cE2uZIG$rrPIqts-*I4w^z7PRianbX4MH9>q7 za4NT-=f%45D7}L#z;CWupAOu8KHlyuRh7-bHs1O)iW`ztk5Pp5c7@a;CLCJO)edq# zPtc%?I5e5aX$@3LiO*UtexJi&)E!32AIutFQG;MlB|pvA zAI2s$x^}l^jO~+%WC#VQui_ws`!r0U;bXb4?r=aA_}^c{|1u;3q3q*QhE1>B8qbDg z+RjQcq*GUcFJpwMFE>~gfjO@}t3G_8jaFlx&it?x+>rG(@~Dy!%TG~I=TM8RVD@)U z-9f=MuZ9WO_6NZj=RqW-a333XC7?t(%N-Sd?*hRfEE-vAiJkP$qM>da$~5bs(*xs3 z6}=ok-+RlON*ln>!GVb$a(wa4-@K7U#Vlc+B|G^P(Zo4+4dCBG&&WA&xP=`jGeHVg;1p%NY+`P z-wB0@(UI+FCKjY7@OH%RdhKn<#j(b6nj$iPvzK4iE&IEm$XjDFwqkASFtEyL49&2{9v_R%he2(igHkf-o~E}lr4e*s!W47rxf~8fKDlkF9<64MKQN|RCG*r zv96-=lpen#6PWlUH!kqw>szbGjws*Yr-W~pxkQA@UQPf5wqKJu8;B{)1(j-z^yq%O zBZCLQ5aHt-J0i|roUC3MWwUh7iw`bNJ%dvxRwaZ}8KTRwb)ECbb^`G~`8*1Llpz?` z_Z&|}0tM`tOTbHeRhI6JckFlr)sqUx)p!H{M>#iXx&7}3?YEm0CmGKu{-463?P_D! z`6y1>eVbSjUs~8&h8HS7eL#v=11sN&E=qfjbAh0Ye?iXuwh-t(sG&c0x{QumZbx80 z9*q6#lgW|@$ZGB76RF38{}HeRuIFq8Yu>eTBqhb>7YbK4G5u~T^RsstAm?|c3mkKS z+Nozo{kAsFD7K!6IepJW`E)rjKecCnGwnK{i80!c(5Kro zea>5C{m^%WuJ?b>CLW%RI`5IK`7Q4HanQk!zJJs8imi{~+iR`DC!U=(C>bZ+Z(ZK9HGU1S67N6<>`5{~4@^I)zPh_qoNt_ z+)96LPJ+%Qrjlk~KrR*5$BDsCQB_mN#hd$HE%aH^%|1wtK zWPdpnre*JQC#fY84uzX|RgkfH4O+NZZmjCm1Zk+g+~7^a2fG;GI`>c0NW|UE@OXp8CeimVYf^=RIDdu@BGvB#sYL7rj@TkLd8h zcvKzTY?ouO7Lu>wNclK6-XySZFpYkvf<-e3DY2i6Sq&FAQ)J{!1P6n8$cz7O*6-`k;=*fz4C_QHLQEe^qwYuh?! zLa~fJp@hVg-``8g%4~^J* z;^SEmUhr~Wl$RF;mQOfL0=oQIzEb*v4)7qBeX`ZJ9Q-a$@U zf99?Zemr)K86}bWm{9jP6*i3BM+IfU$lL!ECPD3S7w~X72eH7o6{?U06^t#!XM5BW zksB+JvZ)B2IHYrScy-rTPs436ll^R#j*E5N)F!ka1F6A%W(TEwGB%EaTdNINB z^sXkd^7Kzv3DbCQ3DZP3Et?SLSe1&7+tKs7|EtcYy`CsOtXUUizw<}6O&hn?S4w}I zFr2~8utQgVEsSF_$bu#1AWc^yCPbO8oC~plQ3M3(Qu$ViNaXOx-tC>~QvEo?zVUE! zy)Hv4!XB@ZdqO#cuSV1Q9p&f% z-V#2mo#ou>Aca20m0p}oko&Jv_>e+mG52a8V+h^%XzN?Ks4KkSonb?#G<+9_`LoLR zP7A*(ykT#j-XyFNnvdE#m%RR)bKX(GhX^>OY$$)Sbv&&ZpU|hj%mQ`_$W~v+wX3RW zU3V2m>MpYVk9X-tTICp3Hhizz-1*2+yj;)Z|P6H@a8nQJXw>5Fl);en7vkLqQt zoDo6LVM%f9tZc)*6*To?0}y2OphT**^;?GWmOXve<|0uQpr!gxQUoBOnFCPp;h{F{BJLS6`JEQ z`;La6g>(h|GY=M8C!f%*=zi zdj00**~+M(SYRu&9~nF+Abei6u4=;Or2k>Uu5Q0BY-j*K3e-fTd;pezgCj?lYLpms zeR1|~?kuN!>825Hhepv)!LGq0uU41RA{@6rxp91UJ-JIM#w#55u*G(9J@G_X>67uW zT*V>T)ztcqa&Wd!h0!2Y*jsMhFH;I+#2o$>859Kh&_2>6MMd_>Hnb;`o0o|RY09!w)S%H8@SAX>pNRg zhnZ@J1s?Fm`hK{oPgX@B5D*Ov3y^2(p#M)Gb0eXkqDZ`tGGS;@^?_a5y0R%~vk~#( z^-9D<$FAA=-zJk?^v@F`_P?>tD_A;ZEWWy)YLD%0gw?nrVOu;DrQ?E($dY5;6el}4 zIylk_i~R`JVFFFsI@6h>$s*}#;y9%LO&%vX5>(dvO0I=e~R2d%hp* z&w8G=s!>&=dRBMyK(EiO`C~wJcn8Z-KHTAwLJ_;$Il~)$f#gJ45k)@C8%bc;==Aw#oR(A{EGG`)jlz&%j{6e=v$FY#XOwMbshUWa!5~)}~qo3kKD9%Yn zZQX~W!fRzAs<~YgQKc6PwA|-WlI)vJjyP~&CQ7E0r@29f__!@E^D1iiTu+`@5k0xOsIEMSc)E_!MXH5Ix#LYOtc*2#|d1Fi0;uRMG6KC%HJ!A9Jp6sPyBhuLW z)`j18?}t!GAiEyg4A%?SZlQ{@lBMVR4hHXzKFa@=Q8_frx1Zj7rKp?SFK{#!8L@w&9+Kby%V6_JmvVBT2jCMV-p z?pAs^=JtAmGxTs1tU5tH9^3rlctb2iAIE%l`fN8bp|NVnSqgyxTzXtjTKOg>1uR;Z z=X|W|)rNvsbkddO(C+6Wocw9&ruqI^WajHQ_u1zlLooUrogU}4lW1~hQ!QV$!7+F~ zW;s0)>94OnEflbMaOBsy1G;*PJAPi8?!-YPcGPwdnkZ&6UB8u_a%YCMt+U-V33A7#;2oM*Ac<9&0!n+%o_O+H8fgF$rYlgmFL}ug9~TPC8&J+g2pS|2pTVKbD9~|=rnyG3^vtwRo zkO6XR;!aD+ox%7&-7GyH(g}E0cLF=&Gf)64 z7baPL*=H9L0o%r=S)MJjMj&Ov!%OJE{%uyxD!ee;Onkh!2&Y(gi;yoj3%a^=B)sr# zmLa(?9??ZBNTdjUeuECG1`rn@sU-JO*ioTPf2Bh-x)#OO3!pp2W$I4Nd+l3mCt$ij zfy9;0?-)~9g6PaE$j50-E3fZj2{lf52fbVS3JSO~;&2Bwy0@olD``nw8K*$P>d`#HSEp&m&GidlMzrb;G`s1s^Qo!^zCK|Y~bniU~ z^sO7{NJ?3D&A4n<1UP$?cVy}bXg5JGI-?84ENjr?H#jGu%R%J%Hy?M;JI9|)4xSn$ zPk%QQ-afBABmS{^#LJEp2W-tQToCdJSKO>_$mQ}EdhRt7Em80(hivSdX610Joipyr zIuaYJt$Nzh9f8^)(BzRJ(4N<2!Qe31la_Y$g`(>&{YGTaqBqJ9l{~19kAnpQz$1je zC6er)_fD)QGe$;0ftMsJE)wFoFql_h4(z|%GDn5Dz=b?{9IW;NKSY$S3hTMaWx%%t zUhcxzpWimOg#rTy6W?G!fn)4^(8BogNmiGsscwM4Hk)hF3jZe*$8pH$QPSamjS!JUc?FIR1T0?`twqU+F14&@U#jaCd&eGHkM4 z;eRh~G6Z_l8Z}~tH%^dMiX_m_3ZUjACY3|i z_>4-H?IX1Hhy^vFu00--Zk@0Q@?1SH1miCz_Xj@yjAv~Z4(yAM76(iR*$p9rd}=d% z__-(cj1r^oy1mC5oQCl*7OlQyyVE0Yc3!9l{_x&E)iA_f)IX)Ut3=a zeBjXjann)}wh~X=oBnbuJ%pAUrdlD(D9iBo$G%rLpmP{!e^KXzQw|s%EH*g>WgCQ9?tv%b(HW z-3pwVRL}w)_Fw|q;qc>Q>77%qqm<6_ohRD*;L90$wqX5k*sB0+R|kPAdqa7;q7TY3 z*~qC>NcN2rW4h+m$P<$HU(hYj_cEXx_z zdg1uM?^h9CkAwWBFFIpt;2c*+YysMMsJ-G%D{mrz%1FHm*<1M!nKAi~BtHwGrQb5D zin8CpS0?}k=lV@_R<&3S1PEOlzX!*_@UQ-x5q`4y!z3MtI**6G0YM*k(bh^p5@p)J zod*##Fbg$=tUdsNMM=nS-5AQXNH6~$=b|5~?+`+L6veTFAXn?T&ISH+P1hZGySVWK zOWuzcpTW8FqvlSvxgT+QbeJ1idF^m7S2t)P{nBXopaYyg8hM}sGvKK&apLd%Xg7=m zvec~=-`nyO8dbT?_S4V$?sgEznq0M`e(-rJIUfY&Q0e$AE)vih8u;-0=IlyoHB6N` zvz^&Pq4WE4NTAQ0=h@|x@2|kGQ`rf;(5xPtJ75&;Ud*vYpx@3% zEMyG`AYYuIH8>wjSMV7jH)_A%5rzi3lW$J?c31B;1pXGT{hF@Fs zL+cGDS~F-sXFcx1*K6P52b9r#%32y)zL5mU@TZfB!!GL3@+8gj;qUZX0%sBj^t@Y- zYPIK^SIN!JuW|`PfGGYD$`1yE=W(VM(;G}b%8`8u$tcp7t&rPZrnMirYa|tRIh!#2JAG~P)D@W!GkYTcBEuD20*ICar8H4+tVw&aUb@XP@VZQ2b1*x}dlkmy@W){iR1 zZeHTWGRX$Q*T|woP1q7apAU$oXV|@cnGUM3WEHg58adO%!9O#~4+Kos)TU*@^l01F z>!54b`6tTmx~F3sqV0Pchd@YChs~-e{D3smc>2K~i6u`u@BJ($ zPfGYON`>CzRpKH|Lj8uyZ*j$>Qs_+_(W(47gs#h9h^%9s^Xb#w`a+nMd?9*nx2=P> z&i0!dXaz>*@LVu|k@kAD9A7_OyXN$}qx9TtlfG8c_asJiVLN&HTX?&Cx)zuBI3*5W zFa174ATDovz7XaJ!Kf;iQ34G}rR-Lu;P|chT#>9#zprqFNNvuCMVBN)$-EuxpL3`g zp1a$`D3V3ctMRoBPxyKG32&J@ct0?GGdHNvqfVoQlv?DVVWm(#h9x~>Pa4^QDp z&J$Oblrz}^?I`lg&%_~Kmk~;)4}#-AX!*Nx$jIXED|!Dj3GY*I63>i7Q~KoaP>dwokXOSG$sHu9mQ_|R|`ctBUcY63pmb_Tw)cJ=VASjnN>ZUenm zTfAF4=fM}Q_PZzG`J7kX`p0v;zxXR3ZmD4{POo0O{zTXYptRC~6u)})F8tae-d_wP z6!XD4wE&P(3U!^A>q1;%Bz~NMZKbtnor;;w%_jC5YevKH~IFP;i= zIUHYjbo5B5Fu1^4Vzg%?{wKO|X%32_ZPac1+@U>p%bKQ0>~j6d42G_O4fT%Y1) zzk6~X!2DZ)@=83y5P|6slI3Y{On11f4{T~~QQN;1&161lSGp~x2T;7hm5TN!u1uHR zh`NMqsysnZTE9B&AbEMLb0m5AT{W8;PH}4={KJFNd})h1>(}oM|mEGuXt{T zr<`|+OY;kD7bfg0A+W8Fd~JYnK|g?>6g(E*sRN`cdEUl5%QHcyYP?26Mk`>i$D+O56)83j^>_d1SbLS_Sl+8Zg07qxMM$7_(fog zIm@G{D^(Q3nFLL7XqRs?dIS3z&hc(2FeL8`g6NKa)GdCmtkgPsE?$sh`&9PZ`|g;g z@%Hz>vDDV%w>Tg%S3N1*C*Pb3_VCfXI1^nddRe@8AKN^2KUiVY)WTs0Vh-09l;~nw zzu|B(_6;0pBW#%zDk{LLl%t=NNp>9Q&*A5I?PoC)27=M!;27i)8Z@9+J?IyM0~TXO zrJQ;gD9L$fylqBWJbEpa>6SD zksFl|@In(Q^U+C_BSlbd6Vz8<~^$$AOMV*gYGD%R~FF-${-b^=!^9_EfjGl`sesnBX zA{Sqz92**JHjSmB4_}S#lUA9x9rAj@$U0!9Xc2Ib#RHDWf}kvXsTB3)Zt8K@&j^} zRcVtwl4GiPI0fQz2e>xV1MPFmnFIX7=k0%?P$4+x{%I*#ri0c!Zyp5F?IdsQ9H)}E z;`S-2T$*(g)D$7Cj8gG)6|u4Fz3I81aPb9$&v#5wXm6IvBxSs;^}}PfbGvf!JT1O!=M46rNn6FBv#wZL+R;@y zqXmiFq&f7mX4^Mt-Ve~0rb=t-7MfUo*ozGAwK{XMU($IocwCfs>aS!L`1bLi@d>Cv z3o^6`KrpgnHF9p05r4wB#m*238>YwA6MtikDBnK`QF!!4x20*d^CJ1{U_}jf1h*6X z=e9AqMDR`zU?4417Y@Kc-#a-eY*gsLW*V?4_;&syG&&_Y&F)VXf-EKcCvs59mLOO4 zx>;L>9?x>p{;&2)x^O}DxqmZ(CttOi^4}IQwAPg-=1tK}Gl>(_BdFK`@)Gs4iq)Q$ z`YwD1SJHC|t3%{QJA4e1uh7KPqsI{k@JNv(jq+TJ-!OzU&L*lzEoe(JyX1>7B!7}) zTO&1s__i@i=rjOR`u}T+2sZ8R>j|Rt=*z&s-v*Bz$R&KrcK*UiKN5#SLPnu=I6KQf zesh)!p196T=Tr;`{zqyDjG)vaaAW}<0X(O8S)M)}2B<

r@R8DskCg$QY%d=x6zD zS7xPZcT!SYuTEDHH==$j{oi%tH1uNt18`7s86HDF4!ZI235u(usyz~^-r(5>KU(d7 z)a7ngv1_-XG%RViwqV*k_ml3iUGtuJ5c2RwH;@=B(7% zUIHUb*azeIr0`R;S`mYr)%2}i+cA&*L|d`>V#ZKIl!GrxsGR!BgE{WX zikccGd2s0p#jEWTh=&EuV+qUM^#~>9eKCd3zORO>t0jqd+kxzl7y30*WYckjFsi}@ zdW7Z-3F;7%bVH>G@CTK-+y-C<7FFqR1ym24xsuezPkJ9-PO8rY;{R#PxcwjUYX=bo z#DLedtyomk;j2Z;>FT^09F~$h_^qQwKs+q8nT=$-vM%K^AwS;#>K*_!zjDy8{EJwe zkbWF+0fMP?A0K@5^p6ImaGGZCloL4E3LbrEKAJe(!U{Ic#~XMHjCmQ!JuUD5$)wy;NV}4N3gCcye(-{fFZ$Y=dT|JX15@@6ru1z1OCt^60t zw4Ve4g##Eb<0@Pc`TfQ?B6Pf6xv_Q=53-veSLQPJ5W*Qc%bXarrzz5ca!wS`=~n#< zLUP7yKzY_xf3 zlzw3`I#6;16WNqvCQ($3TwPCPso-ozcEMn&Uk9$$3=C+n&zqkDLwA~~0dNCF!?Jh? z45xtUg*6>C=$E3&%>3U;zq;w2AF2qTl#}ORej6JkW}yvJvXBJ`Yk zV+`ih|265EX7F`ol|x901({tr$zt$oKiH2dHAS(^7E&(p4COZ<){(V4YoJJ1OZNNK~zrXR&ID*fMF5ECciW9q5w-RhK)J*7V`8yvzm z)HiQ4-41XsP1A?Qac`;5A9II_Zs(*1!FPzZ_iJ2TWCU0QeItJ5#&Y?|Kg81%>)2g zXOVu(>(iM-K~oP~6Q`!6=t`rigOh2R2ul=Sc&M(+&}~Uqyq@GXh!cK#wvHl|siF#@{t(J`3#jPn z_MRgvnrg!^CGe(=56{s^|Ad8ZzgKoUu8InX6yxSoE^WNmK_%R2Lb#Ai#Hf46H(f`^ zYl}47_Bixq1TAW4y1oC%YCjEY^~KY%zeDH>)+>q{~n>G_wQCLnp&oVqhoCFU_X^6P$P0z~F3|<^%e8 zyA%8@oYrw{DHF<1_{S!F@5`0(>1B(f7x#L9f1L1*<@(X&z>>APke>VT`~3jv)-nT^ z!v9A39U8DB#{+VJC>MtZ88=2-Z`xY_FJ%Tx`8+)~PxU9wrC@fxEc}rt;G5Wb!EBuc zzR)8}%Q_ROKMF>9-v$jRo)7;>nWH=c0P@+HY8wQYYW+nr$yl1eZw+t5RMlkh^=cP) zK4q-8)#3^29$wd*Vv4PYw{+qKx<9}<4eQVl|L=@Ip6S7Og#l12A`z7>Gb#TO$F{}) zB@)3mOT}a_N|;Q#EbP+Kz^r#D)sRuWO@1tps@J^ zaK9f3JjS2;*De#TBY>&Gzd)3(XaQKQMe>POOkLT_L#oudq_E@>)g>eg)qAwF2>Dq3 z(qEl=;ljDCgKe*#@U^brk^HGBm}`LvR_SYa?mhy2>{Zz+Bq(aQEQGn<;{)0s_7m;g7Ta6$pHPzX!K*ER-yQvYT+C9LH?1j}3Q9Bdd;WoH=K0Gg9 z5)Nu$;U#{}31Wm_H!@?uUI1D&_@fgcKRPlxas3!^d~C$JDSBmH&_2DH+`9r_e&@_C zg8qBdVmGDX{lMV!mf-Ew_;<|8xoW(%vHz1o&hi{^g0S}S8McKwO0d2WBWeB|+ejY0 zrAl5emzq5bV`}%l--dQ7?}6maIi#rkyd(=hZr;;s-WC(-ouieFA{1y|Zd zKe+QrZhh1ZP<>N&heUl_|I`BPqB~Xb(L@9_YgA7)eyS_3%!LAiqS7Jn2UuWY%o^w; z3uUymR1=x`tpA$Xdz^I-tw=54+N@Y{d4jOo(9iP`6mkaNO5UW6}ySjXU$ET!J#4hrxX3u zZOPCWwBD`mhkH5KzwMh_jXERBbGIbdN=|u@*B;8^LPlmM>@T zAGftuKe@7yc1)2)1Vw>aiO8?N)ZgxBakpL`PDt0yPg}8~QiK%=@$~L7IKCG#gFC~& zr&gfs!jcOoarE@u$b~mOqvPO7sMg65(CBDcI9*sfs@sb1vfbnlzUgvjYs_WfRu4wJ zT;T|G+T#X1ZaCXmVBW_)4FftP4%d&900l<%%nlC1}dxMVXa*#$xa2TFyY zZxB&ZLc6-WY$cBHh^!3pI9!$sOB+J+f4feF*;`n2sx}FqMKCY?_@p*=pnLeR`d7Cl zOJmJjD>v$*MgSAe19g43~wD7Qo_Po-D&( zT1^}7r@p)aH^(dkP-}t6H-V=r$R~sw=*rUMWHZ@)VJyN(bKmYTz?BL12jJ=XWoJ1s zjx2NP0ze!dT0+M<;?z5JjA2gipXMntMHm5M12WJxdS6>K(?B?>+j-%Lye|{Xp}@1O+q!jjaMC2%4Na3>Kd&@7BNWyM^XDQ&^|w zRcxFfg;35V@S0NOo_-6yz-yfdNk=Je+Xf`)YClmyzfBSvYddfQM_k}} zDJD#`vzh+Nt(k6j<+(8QB1Wx`xF;R4;8aWNvY%Qo3PaL!*O-lwa)vRQZ>AlMNeS1m zb}V4N%^!*$TN?nG)f2#Q$Hske4u04}r7 z-9qbS>TZF%u?uz=9%Pp+HV-#>2h?H#kRmY+g)y?J))Ih@AKEdkQ+Z$LqK z`7ANpeSgupv|mCgLx6Lgh;Iqz84)S)2kR3h>=g{=fKVK z^oD~7j%X#;&fD_I(M7eNq3MyCe&gu@$SjH15}WI*J*T-I*X=U_y~5?MqmCcA6SwXt zrf+0&>%-#H3tXHz9C@923p@;wfW`stQuOo3RW-B;#-iZn1iRMo5%22z^J!M=IbS~G z*+N5W?;06@sa%HXhRClHATg3h<8xU2b3fPHJTSVL8bjMQREWBpZ1>k+_}F&*mLy8z zC3oYir@i)t(ArJfO#8Xrxm-(;(?rwmW`XTE3;Gi*ds&USQaU z$>EKED&D=56^5lMlZe^~p@oIkeuBpU%HI>k5HFcX1!`i&|>nXa2 zqi_(e2kPc}4LR}=emDna>C?0py!OOGcuj1Mc8fF0`xQ5KE^rFRm0m6=rfPI~kNS6y zjzDomiz6x^jYo*}xd*2E8J^}VeUz$=%)4TKfwg`E9>89p$E=8G#yEQ%x zv(3g+D2J8-gi9tbB;FcES-_9@_ zw*7b^sN^LfYnZSb|BdzjP@TT6y}0x4K$kmrhGiU6dqG2}E!TbDda8?=v5$6J9J&o& zR7gTSDM6)eO&Jz(mXAk1;Y;>qmjpXoz@&qNyf{3B@J3-9Q!H5Qko;AREokJVSWft+UWDgh`@n0hXO1L&kM6*5wHjg(mpJJ4kpr}$+o%u0^*7jZquq00B za-H4ZAU|2H3?ys4YbqMAbl=i3wbhwbKd}&Qnq@MM(SYKm$)4)frEHuikRt}xv5!ln z1^2#moe1?hc^7xv5!q_d%VD<4NIU17>LpC>G``cR9vS+w60$ZaM_T6o1K%`e=FRSK z13<3@60S(_ST*$z2qRcQLBUYljoPO1pA&-3R?&R8F!fl_DhUkGQp3Sf`kJ=M6O+hS zr^H<=x>{jl_yc&R$WCL5dpzGc^@j#ntn%g-9j{ON2W4d5Y8weG(l2>6w zVvljr%~JS~#DaW8dSB5{qkK5PF@UGH7%2k?RWb@8Y3)5z6w036yr%ZQ? z6VEh;6AuC{_q2ys>ja`!_f|Jb8MXk=d}o@ly8(zJMSvH@EP*STWfm9Wqg9>6T}7k{ zonsEg7K3L3&t;gexOT-a&1Em%y(fqM8qkYpdK8MDO|~0~{vKXQfo~%ZHQy8UVTU9L zQ~W=<{$8$Kgj$5h;uk+T1`(UWLKAdoV?1p4x2{s{F|!u$@|Xk#AH{Z1DJGj7z{S-S z4?u;YkEPCS{1u=~(cBU>@@n;+?F3?*GNn`P`ZbrA=VGr2XSc%C9-LfUc*}z!qp%QY zM1(#H#QS~y=8>7!{)Z^Gv60&oiJZrcZ2*s-Sq1%c6lFV)doeBBtynu#Mv7w0vSBd} z+Ja^M`nVHKuP*hy+nOJHX3h^=XuGgOhHi_kDz3Y4F(%jps;Na>VLV*C_!Zzxi&&Ea zP25;-h$%87eEGnxE7c$&d!g0^7yywh7c`MB4&hQZfC#XNr>=RUY(Ps}skoY4VMdvX z0EI0DQ>r*io0>QoV{z{qZxM0fS>WOb2jb~SJF?90FRmCoL#Sn_l>_7?J=IS8G7Vf% zTLu;+AAKc^g`6b5Jn;aGKBBl@q&&N23gL`yd<@aZGc`xb3-b*$>i}L>6gniT7`zA> z1l}5=x!*_g?cJ|AKe~7UF!zJsTUy-ZL&r~5DmzEd4_2jP4dY4U#ov#a^?5s$=S$0x z(h&kLz?#4|FwX`L4?#YJun{U#;N!XQM>J@LyxCzT+V=!mlfmMv$L)UY6PLF4do~BJ9%**9~)K)>Hpc zsG~+C_-FB9DGCl^c5Gt?l0hD})O-vJ4aZ%zoM+@`Egm!#{%5~Cmmq?f$(TRkf4>v^ zkYD8ep{==sGm2qrzV#9lN%;y5&rpRviz$Mc_T_0PJ@+g~1QnP8D&iS%i>05oEvYae?M=0O)SixB@F`Cxk(}5g^n(kG6yM!aTbf?tTy5a z^}&ofBB&HK0w`7|+I7dn4NgKV6eEIbV~ydE%S;qw_G=GZ`>mC|MF{^2sgaAP#vt_f zB5xm|1sFnQZy|xSpPIR_twS+u+(wV6!R(gd)X2yqxNdaJl0ubr;oL4XgSuG3K2Kk2}zg$3pBm0nbrA>kQa9Rtp25 z1?plVbR)pQzxBhj4dYt9b$ndl;XnKZB70|UgAHPf0al8d@N||sFa1u8F1m=Zzc?RV z(wP}meSn^yD14#^*8?Sh7x7PW(=!mmHefS&;RkVh2Egw^CjpZhi=OTtKjMs|(Urkx z5P9;_#fDj<&FSLQ4d|0&O;ZySNhVZLNS0?_s!eJx<<(2@*$41!dA2Va1N&Uq>_zb4 z1mK+z%0h#F-k|Q`g$}2X6=uh&i^A38VVU7=D=_{dC)4}^1t%4dN%(UOp?msX95fF- zZIdG`VYE+!7l>|1c6R;Uik&&rT6-bRzsjTJuv*~d%Ans2@ow;s>75?sCrD zbG#aGpj<{`;`4|{wOx-y#oH!inSHBPS`92|A|S1xTCb&`<167IcU8m{6ICWQRWCGt zdXa=~`)7RrIHY!El0z6G^-Urv+MZCKi<XQyxy;~L4HY`fKKf`{%0JIWGKJc1me+av z%_fHkr&&-UvG)Jtt?db$zdSYKCgbgix67!@y`l1#l}+?mq}+`Pbsyt_A4f#vq7C;I zT>CUNT8<(HPcrfo&(xh3b^CclqMnXlztCF_d^lz3fu@O-fCB`2CIc9!F9oDS)*$;tk%3)kM4D?{1NTI0<{B&=6 zs1LsE{Eau<)`%Mi7G~pd_xbd>`aE=FI$EU~K;=Xb|w7Y+5 zN2J>P$XOHV`FBuQ<}$4EE}5MlNDKX{Vdzkb~V4Bkbu4IBikP{WO@k(K^Tq=uu+ zsa5=Gfppw)y_4E_b@of?wj&R!dy~D(NZHVn)V`N`iNGut*WAszZzxG!g=jmNmgQ5i zNlH5VaM|w|E9(e1JJ<&_eTOEm`abc66RL2TcK-@;d9_C9W!In^8U+f)&N>o{tEBWm=2QawvC)baxT7rE4K; zo#{waCXei`9T(TV4dknZ;XvUpAbfH*G;TbLNu+k<+};{WF~JY}qR#%dbkwOM_H;p! zd&>e9xGE9gIa}Haf$C7z#~M<>*!M^$8`rVcr%0`Rn@z>Q7+Q;JmgCV+MtWV6nN

!LxQh!NmF zk`(0Tj<ZW5E8&DpXim*G3_g%p~m+WPP3v>mv`TF{tA#CZlAV1iI z$sbTihG%;$dY@ID{z$-{<*^l96ez;5-yPlm9<#0Vp(DmRMjt5}mO&)*|Ao}9w1NT!U!3v{BR>pG3m4dvAv-jR< zR##iwb^`%3Z8MMRLF*iNu zmF$BJptdopP98FT%?meHALC3)l*?Eak7wgYuQrXJ_Eu;ZwiG;`^w{1|Op42fT9g|_ zsshz40(mPpNJVTIg;l?C(@J!tO!HQn8OY;fg^<T3@U|Sc83Nq zOA#FoV9fdJ-PZT9?Dwwq6Kp+yW$6+j$E+2gEKV)%rCetTnm49eyPU7DZk#q%+pW~y z|B0KsLwG6D4WtEQsJaB!E_;~4mYqb}Yk`PDRW*g$2-eD;kqA^yXCsWBh2TB7O9xEH zMx{eH8;>r~9Zc${$noP^Mu!$ThYQfe*2=(d(b)gw;QQqxDX3?It`b{AekTLUF7Gb?yx`*nAz;?yh5 z7*w$JXKH%7VCZ3i~H`= zlE&Bef$)~x%WH6)Z!*-LrE7XzE52Q^$yk%qEh{E$wz}DjokiB|S1m0hvNysa2*7yx zRs-~^|HTL(s$@{Nh>l8Kiq&75AW8<|9L$KoUWACkuW*yoq&mH?Kuqg+SJ6RO&dhv<{j z_$#!^w$(5>*r|^qC%a+CrN&>={f#9E(8 zJq;|$^KYMHGhb#$GV`L9DA&Tu9WVS=`^R-~QI^QPnguaC>2Sx$j= z-DutV&bp-LooA|saXYm0#D?5tVq=*c1p_JM)&sN!Dh&h8$0s8+pI&UM5-VKtLR^|f zR6s3K=#)Hww>x}V{+W1FY5O9Ni&J-Te0)w}mk>YIY2Ed=li}W)<=Itt!TYdl7=Sh z;#qSU=Y^){>z%OJwY9FtNm9bWx!lVEY)KUXd6US~c+`>?LTC%RKVj5XescPDCNiH? zhV{psT50&XMGX!8$9>GU06DboPbf-;-0P_;zwX6JY=spxmQ`z`k{a_CoKYvuhcgq% zgGHcIBXvS5q}ECb@4U`Uv3cE^d^1{?t!u$Wa;s?YK3?q!GiIHHU~SSePBrQbcrT5l z8M7Uyd3zr$y^wcP^4z>MVgQId{@6WNWId7%zhfB)DWt2*V6E}%sYf?CA#7BqKby!E zU`eWB^{=NNul)2nr$iTYbH8GLlSZZt!UO%d;Gt)uXYZ?PO5s?9?A(C0%~LOn+LFYj zHJ8#<=lC|m+5=%H0=Ljrao{)uL_UwRPr~}iSWP>_`@!$uN^%{oWBg@5H}L$#;Jgdf z!IXj`;4o57A;S-*j_r!|EKOdBhcG&#e_(%em3P_vDnCUDx8|3VoIqSZfy-a4*ZSFi z#>Vwd(~o)X^;%H3HdsLQ)%uy?KJ$Ks4%*DVf(g|Of%ZgWvHhXL5>dX|b=*{QpTuS< zY+o-T7n6<7X)^uUH98tYoijiGlpxU8=iLh%+h`TXTx8j@M=NbKq{zu}{EMHE&bJ&f zAvqz1V6+C09m3dYl2P1^)rViuhQ9oUch_HpDv(~XwWJ(y(Lw-#dmXkHB}mJ|u;={x zv)>iJy8ehjP`Ptfx=}ZLJrfVz4?8rtG}uDPDR%?+|^7l}C~k1Va1A4ABL*j4-5YPgh@0zLBW4rw7M~5mML5 zU!bcsd?0e)a*n=uTDm(cjd0oa)goDH2@JvjqT{!l9z5|MwH$j-HfjtOJPEe6xw&HA zDzIn5mD83mi{T8({+5!(lVr1+%ufiGfG;>$l57u3ymgDvWU zPfwnwk6F#JaGP5ya|5z-+dBNVSr)5JzkXO7?QI@E&vE$e3${65=*1sZ1fr=ziku@< zjp59%EX{DohXgIT^5uS*`h-g$`#Ch7w?%%4F6?c$ta`y)eHe%+QEVtHdycoF>h z>`C=EU!DYpHK9;x@0ss#bP)NseFhK8=&e;J!@F*eTlpZWJDr&>T+)`zhv~xpkd@?P zQ5EB<#nIV{q$a?8i@cpKmgTSwTpjDk!xJBx{Xo}3#vHxLOR>eD{GF$r|E^dK2=taUu+Z1BeUQ&{hSd$r1wazZe!Q@+T17Tv1}>tf2fd$2Nyn?U$JsM zyB!4eW0HQ#l)ln>Wt^!eESx#I=t1!u;(-OR zDi4rxJ1lGCK7(2>UCrQt9fSKQKgXhyV{00YtH5c*kiA?>!o#Y21M6-c>1(GS&vb?F z9yE4|k_CZ*+RTjnz24dQR$62HNpv3@#JNtx=XZhbXU{9Al6fbRd8hr&ge*AL^!A^9 z^U@3Zd)NFe-@uL9Ourm{4U}GIi@g{AN(4zuk}-o1#ck|)%75<}_}9?Q^QNBbQCu&D~>g|fXS&?W#NlX+5tOBn*Rw|xJT!O2)0z3GsE5^92gs8eQHcPOZ6*%RHF^Y6E_UexI^;&$1e65lU8 zzURAt@cfJ0&H~>Rci8bHc)f|H>;S7Splj*yt|YG z`VqJOyMSMgSBmLAC8MjFaf*umWyrGe)rW~+atV$GQ%a2SPii-~BUrA!*VhR?7FL`F zkiI!iUflzM>FUO%`#ASQPuj8XbDU}d<=UNWq%{tXb7F8~VgGVO4K5mhK}0gNTIuVv zbu}2c{L<(GpYN3|^iIlX*9mQd9UZw#X0FREN$XWSt}9&F9ur`kTp}?G0|&AFIObV;}q{3H7ifApmbTo~QAxrwPNsggCKC z%C*}lG-p1?$%34*^c7n7g_zzyPqGA^oWo}amf!`cU~q{Z?|mWzKzMR{I`2Bcef2*k zxF4R>En2%3i^?wLg2~Z{PVHi@}_fmO8#q58n^EZvC zaZGjXuko-eXOFYtQr9!JH92}?8jo^=?2Lwdc^`6cuoE^aT;)rJ3`V|%A-c8Bhn~(C zo-D>^p@?^gq64cD*4jHdGQavS4W?FEo_S-Lui3k^l&Wr_QUGNEe9PeO`;m`1If9`p zR~sKiEWA_qGtY{<*dVIh1)KfnS_Tq1u(iy}Dq<>!jAFkbLdhr-atXfsP5Eclsj!Rh z8|yNiG00v9i|rp@Ixc#0UZ%M=43Mp}#k1XnluVb#$j?kB7?)(1Zr#scqFzJ>;_vug zNTm^g{^aY#2)Lv+x%3)OiJ1ihh^?V(`|)~88X0WDitL_fdOaO3PK?<>K`2g)OXRF} z?yB##-p;0_70X&NTb0T+e4sUy=2sZ|@I0(e9RGZY5mGF;NzOK(^v-Cyxa%F950!K< zXS=z07oU8?QasUbu)6ZpEVK_=11R%L9!%(2PclE#2b6Z9uDD%ams-~BqD5>D6~fGH#FaxfGEwGY)El-(V?X`)dIfhir*9wFvi!M>Hd=|o zZR41~CGOd!&1y}aT=DJx+s2iu3-);X78#0PcPdkg9pokYOz{Kq*UV_+#8yYrmsTI zh{m(w`ao`s0^ek=1|crEcTPYn7kahJ4(OQ-v%^NmMG)0iou zi+A?|zsN`Y;6F{vO}W{3;57*Ww-%S8i{ZzOaK4a$lHxfWEkwRoyMLM1`ek`2ZjC{A zvN-f8)c`{;SK>zW$nR@pEwgz`s3OSY)9bdpr&<4tqZDbnA-6aL^sEtQplCDIk3;0s=4C zSZ!%Be>8dw->q-2iUW0s=WSIC)nGV-NZIE$iXud120g7GJyK8n?&JPHXRbY~8s-vz z3+}%p6wnG`w%I?P>=`{uExEOr$XyU!Fy0Hq(=quGX3a*J|FzFbTVy(MH{MLILsLY2 zAa{&!F?Y07%Iouf{w@n5wqfT89~yAFck4ae3Q;o#ypX~}wL-adz+({02ZUyt`{vt6~~LQ6sfJb|+Ap4Jx3b)0?@``W9(A4h%AknA+m4TvJ@d6JMrj=!H{KyB4lYol(CCs$yl-*OAoS>wMcd; z@=ovjFTB@vu5*4p=f3aj+@H@m_vfz9Yc|Gbo9tYDNzd*ERxWShzDYjGrzN&;m^1Ip z9@P)QcQbdi@!d)O^3P_B1?VT6ZmMSh)E~#OL$8^xduQYc?~OIQ`Fkb(%p< z)FXMnc=dddZD%ix=jBXF=iVB+fH4}ZoHm@VZLm=)URkHeRazxApF&k@=Yp&2`capl zJq*!Gu@@NB#zQjhxBm>JWl`$t5TUp}w8#)N#?qq0`K0g*Z?V>Xq9PQJdUthN}xe#%^jYy6N5K@^*a zf13LBH#zJb-5IqubNR9#`^CG$d`WKh>>P8SZ+y zGF_OG;hkX@U-b=rFEiiC7-w@mj`{msUwTWL%xUEG_nn9U?jJ|>*8!=#Yiw{ZFh|U7 zZ(f{@^d@TK#&42?G>5tV8ZHUJ@P<+!m~YBHD_IQ{gcAO(@8tu#Az@J2>|TOZUpE)# z0)H)jOzM?G3`h~;k^ZkDFZk8i!Nv;N+NmnN*}3!`Ce{(XlJRLsLW%Xo9v>Y+7OAb^ zgxebXY!45DF8#7Kem4v$b90c;_cm^TE4-1ue#XJm#n>SO%RR|C`I^n>z+WwizJ&Q6 zYDVHxX8IZ%c7M_=rj{cd>Cw2mxp6CMwBrQFxEjXHWDhp1G_sc$D<$6NS7va(VZO;K zQ$AF+N>jb;dd(AP1$bRb`Jy`sx7w+(Njy50R9D%Q9ty@8o@$20;h9)irTN5_N&N|) znV%Yjd>bbn{H&V2;odb187WnRhG4M`(c2X{MK+wm&r9mmmi!I3>zrbpE_C+2RsUX0 z9$J>tGT5|ygY0qGo4hmN{b=%KO5>8c@Y7*tqsSVzMfJy58lI^5_!drmsml>qkI;R; z|KHSyRvVqYS`sop&k%_+o#tb&?QOngM5RvygtoH+UfvT{ZMv-BD20Q1a&`jiD>Kvf zjZ6|Bqawbe!xI=nAt)sZL{glgtBn<0XiyXl7C8@7l4k%-BrtbAV^y4%#A)horoIq5 zH>PI;Cg`A}0VFWAt%!jHq&Gr-Y&kMPEUg>{ON)^VNN42=07izvU~wQydQh!mcz&)h z`m0iPi>wgVBs|A9K=Fti!fhgSL*rz)V?TKm@=<0(5ycKd10JxuQKWy4;f!WT(~1)9 znu`{+Gc12E;hLsYW@a{a4Keu=Ng$4ZrvOY#l119B;&BrY{JG%JOoMUTdky=S1(YM& z*PJw<8hpv{b#v}X>R@_>al@|#CTh)2*?*A=_b9j>puWS%x5SROG{GPRM@)`^kzr2X zZlztAG&S+BF{!sKXMR*NnVM1=7z-f^Ub(?uN2?@MR>dMSC**5jH6l0MB*Z1SZWN7P z5)nO5L7x*zl;L;Lv&uxUr0QE)tK;!lCI}v@&4DyWa+vkn+SHiSvc%84mU)4emO*gH z$RH4Oc#0^M7+e8iEdd6?sEOFfXeoe_27pwV?(IcA7z)$1UX~YlstnaEq3jBRMvfQrDbyL{vA;O|7sRvqy@88NY)8)r+6HVnUi*oRBMU6cBdIv*6h! zS?zcgJW?`L;Rxp3d5tzM#y(Y6w1>TZ32o@BwN#Mc?YPvrEYdhP=ErB`^^; z&Vi^jgnWF5EXz5pF`37rR~pvAc_R|@B)N@ms>pzgqMDcpd7^XHEwf{;@9^MzyuPm1 zank9;g=4Qj_KQA=#Q=Vx_0bg|YD31np_B*Hr>Tuwg?d(}e4Q6ez)7ytjD{SqzN%e+ z+&%ggAfLjj*V2{IF zf0V&0i0NLcpS)HMpcO0Lk``KbvZ6}y4U-fwd)3PR6+;*cAyXM=huqyi|8ZN+p{~8wLc>HfH|>i9p~F!B3>AL-QaHRQt0d|VSP2u6|W6p>ITK3rQ)EE zhvjj)cus9Mrc@Gjr7-xbfA<^L7X`_=?ItsS=skkk&iloos&DSaQG&DLT zOI{z1Zj+@%pWx#tM|eGAJiCI3+gEUhQeL)X?7UcU8@L z+#q=60Mf6 zT1pdtf`IhqRHv9bcEk|OC0=YaqEfO+h(IN8SC}!Jv^?)1IRtl&xL-OR;ZJVN2nlv~ zzh;s<$hIZG`MZ`*$v(dgDarre%yM@`=x__0TA&BK^hlV5>WJmQ(>DCAK)jz{^hRNHj_HWX4zN^0(Ci^qt_IOHLwE!ZgPy?GEn{)>N(5xCD0KrXJxI}6F zM)}xiL88)b@o0I7=Y+uR2R^wICq0fc5u5kk%RQ^J&%5j8ux7l{{M`=4droTKmPtaPfGI|6j`F z6T!vd*UKki^>*Q*5fkKDQ-Zp1Skl03k&wpH{$=K=!Tm>vANw5{dE zFGXJL^NRGI5L4#?s*dXD*e1gs7*ea5LTPED*hbKWDKVj&Bg}xlGJ9Hx3r-bL~Gy) z)(cjuf?Po7%>I)vF<0B>e(^+1PqEpklwSNDo4a_CF18wPc^Bt2S07$Y8%W+h3``0S zUgUWWo6i3-1L5{RPjJo&{@^V;{;*7Bx7~)hEGx@`Kb4D8hJ6+4yw7a>t3;weqlO>t zk*|E0?UcE3!ZwyQoMyt93$mNy%W2q>2+AS zdrI;Y{81eFbo_NzKLg{_8;{aB?QRu;x{Bhbbv=S#ansqHS1AZGm%Ek1iaw$qAh#;G zWf=jwYpr&uYSg4YNrW(pB*RrI7Ga%S>M*QDPH4bT93jF`>b**9yc&^YXS9Y5h6_KL zROe9h$tv7_5!`+GJ66Wlal2%kW_$!VA}QKi8Z)wLum?B0mFfY?Lp8VJ$TPNHAs4TS zI;ArieK6>ARy8_aE2$ZjbHlN|DjK`U6Mm*S+#YsXxh&g8T)CD$^929t_PlL~x3we# zy(f4(MvL8>qYP*9tFxA(_uo|D7dyn1n1*n_yBJuQnM2IJlzu4vSumI zkROW*R%8c`c)U3&65u00a5b=C;>1zjFc_E76P5>Ux=L^!wr0Gw3P*;Q?PFKflf}#Y zM17n#PH=|BVR=LCDJaEYZh9~AW&C6dO>$1H)GF`1nu6r_<;E*J%h@2n& z#Fi~G{qA<#K966AlV!Z>LuCD?fBpiMoUIf*ym{W{rkw&Eg_)&-zT?1&w?4O0h+AwY=1iAheY~^6PF;k84Goxr zCrU;V$o-|Q#abr|11^JxA(J(N=Nv5arrp;&alnAt$Jh*bmyhHQ^OmP4CP+5lW1QtR zr3iM#;UqbS1$8V6HY5uxVQ;<1`Ot( z?M%7-B`tTuxO1v>Z<$;@S*Lk^(wo5S35U*|IbSziz1V{*k= zKEZ3z4{68|?Bf1Es!!tnd4M4YdweBXu|J=9Ph-kX>)=BCYdW=kOD98OyTCj%pyrn? z$=k+@=D8{&weMHJRnMPdc|*8oRz-~2A~Sw|fG4NEh?yV2!TD3ajzEtJ(8h&l>Mf72 zt2ZCzI|W_4rlGl=IVZ+}$a&H`)h`jdw~>?)Us7<3PsvKPj3ouu;Z$NQ^W;mv3c|Aj z6g^7nrdL~vhal}w_IqDbS?Uy+V?FO4_79qv*LjFSu)1qBjJ6Vd{>~f(y?eYJvPO-j zL9$GMuj^rQkS~AhWLaeZKd2RiNA?IE_Iv=&p%OmMYqTAtx7AqMfGakXZIhw8(%jbH zUr^`88=yjp false; + + public IVirtualFilesystem? DownloadFilesystem + { + get => null; + set => _=value; + } + + Configuration _conf=new Configuration(); + + public Configuration Configuration => _conf; + + public async Task BrowseForDownloadDirectoryAsync() + { + await Task.CompletedTask; + } + + public async Task ReadConfigurationAsync() + { + await Task.CompletedTask; + } + + public async Task WriteConfigurationAsync() + { + await Task.CompletedTask; + } + + public Control CreatePlayer() + { + return new TextBlock(){Text="Video player unavailable"}; + } + + public void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer) + { + + } + + public MediaPlayer? GetMediaPlayer(Control control) + { + return null; + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/MovieItem.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/MovieItem.cs new file mode 100644 index 0000000..d0bdbd4 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/MovieItem.cs @@ -0,0 +1,16 @@ +using Avalonia.Media; +using Tesses.CMS.Client; + +namespace Tesses.CMS.Avalonia.Models; + +public class MovieItem +{ + public MovieItem(Movie movie,IImage image) + { + Movie = movie; + Image = image; + } + public Movie Movie {get;} + public IImage Image {get;} + public string Name => Movie.ProperName; +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/Page.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/Page.cs new file mode 100644 index 0000000..ef0d268 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Models/Page.cs @@ -0,0 +1,16 @@ +using System; +using Tesses.CMS.Avalonia.ViewModels; + +namespace Tesses.CMS.Avalonia.Models; + +public class Page +{ + public Page(string name,Func model) + { + Name = name; + getViewModel=model; + } + public string Name {get;set;} + private Func getViewModel; + public ViewModelBase ViewModel => getViewModel(); +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.csproj b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.csproj new file mode 100644 index 0000000..2726913 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.csproj @@ -0,0 +1,29 @@ + + + net8.0 + enable + latest + true + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewLocator.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewLocator.cs new file mode 100644 index 0000000..defc095 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewLocator.cs @@ -0,0 +1,30 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Tesses.CMS.Avalonia.ViewModels; + +namespace Tesses.CMS.Avalonia; + +public class ViewLocator : IDataTemplate +{ + public Control? Build(object? data) + { + if (data is null) + return null; + + var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + + return new TextBlock { Text = "Not Found: " + name }; + } + + public bool Match(object? data) + { + return data is ViewModelBase; + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/DownloadsPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/DownloadsPageViewModel.cs new file mode 100644 index 0000000..6fce003 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/DownloadsPageViewModel.cs @@ -0,0 +1,19 @@ +namespace Tesses.CMS.Avalonia.ViewModels; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using LibVLCSharp.Shared; + +public partial class DownloadsPageViewModel : ViewModelBase +{ + public DownloadsPageViewModel() + { + LibVLC vlc=new LibVLC("--input-repeat=65535"); + + Player=new MediaPlayer(new Media(vlc,"https://tytdarchive.site.tesses.net/content/PreMuxed/PzUKeGZiEl0.mp4",FromType.FromLocation)); + + + } + [ObservableProperty] + private MediaPlayer? _player; +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/FavoritesPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/FavoritesPageViewModel.cs new file mode 100644 index 0000000..3a6f72e --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/FavoritesPageViewModel.cs @@ -0,0 +1,8 @@ +namespace Tesses.CMS.Avalonia.ViewModels; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +public partial class FavoritesPageViewModel : ViewModelBase +{ + +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePageViewModel.cs new file mode 100644 index 0000000..21829da --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePageViewModel.cs @@ -0,0 +1,29 @@ +namespace Tesses.CMS.Avalonia.ViewModels; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Tesses.CMS.Avalonia.ViewModels.HomePages; + +public partial class HomePageViewModel : ViewModelBase +{ + public HomePageViewModel() + { + _currentPage = new HomeUserListPageViewModel(this); + } + [ObservableProperty] + private ViewModelBase _currentPage; + + [RelayCommand] + private void Back() + { + var page = CurrentPage as IBackable; + if(page != null) + { + CurrentPage = page.Back(); + } + else + { + //can't go back + } + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieListPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieListPageViewModel.cs new file mode 100644 index 0000000..b610270 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeMovieListPageViewModel.cs @@ -0,0 +1,53 @@ +namespace Tesses.CMS.Avalonia.ViewModels.HomePages; + +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Tesses.CMS.Avalonia.Models; +using Tesses.CMS.Avalonia.Views.HomePages; +using Tesses.CMS.Client; + +public partial class HomeMovieListPageViewModel : ViewModelBase, IBackable +{ + HomeUserPageViewModel homePage; + public HomeMovieListPageViewModel(HomeUserPageViewModel homePage) + { + App.Log("In HomeMovieListPageViewModel::ctor block begin"); + this.homePage = homePage; + Task.Run(async()=>{ + App.Log("In HomeMovieListPageViewModel::ctor::async block begin"); + + await foreach(var item in App.Client.Movies.GetMoviesAsync(homePage.Username)) + { + try{ + _movies.Add(new MovieItem(item,await App.GetMovieThumbnailAsync(homePage.Username,item.Name))); + }catch(Exception ex) + { + App.Log(ex.ToString()); + } + } + App.Log("In HomeMovieListPageViewModel::ctor::async block end"); + }).Wait(0); + App.Log("In HomeMovieListPageViewModel::ctor block end"); + } + + [ObservableProperty] + private ObservableCollection _movies=new ObservableCollection(); + [ObservableProperty] + private MovieItem? _selectedListItem; + partial void OnSelectedListItemChanged(MovieItem? value) + { + if (value is null) return; + SelectedListItem=null; + //homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value); + } + + public ViewModelBase Back() + { + return homePage; + } + +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserListPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserListPageViewModel.cs new file mode 100644 index 0000000..6a0eb4f --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserListPageViewModel.cs @@ -0,0 +1,37 @@ +namespace Tesses.CMS.Avalonia.ViewModels.HomePages; + +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Tesses.CMS.Avalonia.Views.HomePages; +using Tesses.CMS.Client; + +public partial class HomeUserListPageViewModel : ViewModelBase +{ + HomePageViewModel homePage; + public HomeUserListPageViewModel(HomePageViewModel homePage) + { + this.homePage = homePage; + Task.Run(async()=>{ + await foreach(var item in App.Client.Users.GetUsersAsync()) + { + App.Log($"Got user {item}"); + _users.Add(item); + } + }).Wait(0); + } + + [ObservableProperty] + private ObservableCollection _users=new ObservableCollection(); + [ObservableProperty] + private UserAccount? _selectedListItem; + partial void OnSelectedListItemChanged(UserAccount? value) + { + if (value is null) return; + App.Log($"Selected account {value.ProperName}"); + SelectedListItem=null; + homePage.CurrentPage = new HomeUserPageViewModel(homePage,this,value); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserPageViewModel.cs new file mode 100644 index 0000000..dd77db7 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/HomePages/HomeUserPageViewModel.cs @@ -0,0 +1,55 @@ +namespace Tesses.CMS.Avalonia.ViewModels.HomePages; + +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Tesses.CMS.Client; + +public class UserPageItem +{ + public UserPageItem(string name,ViewModelBase vmb) + { + Name = name; + Item = vmb; + } + public string Name {get;set;} + + public ViewModelBase Item {get;set;} +} + +public partial class HomeUserPageViewModel : ViewModelBase, IBackable +{ + HomePageViewModel homePage; + + HomeUserListPageViewModel userList; + UserAccount account; + + public string Username => account.Username; + + [ObservableProperty] + private ObservableCollection _userItems=new ObservableCollection(); + + public HomeUserPageViewModel(HomePageViewModel homePage,HomeUserListPageViewModel userList, UserAccount account) + { + this.homePage = homePage; + this.userList = userList; + this.account = account; + UserItems.Add(new UserPageItem("Movies",new HomeMovieListPageViewModel(this))); + } + [ObservableProperty] + private UserPageItem? _selectedListItem; + partial void OnSelectedListItemChanged(UserPageItem? value) + { + if (value is null) return; + SelectedListItem=null; + homePage.CurrentPage = value.Item; + } + + public ViewModelBase Back() + { + return userList; + } + +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/IBackable.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/IBackable.cs new file mode 100644 index 0000000..49ec33e --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/IBackable.cs @@ -0,0 +1,6 @@ +namespace Tesses.CMS.Avalonia.ViewModels; + +public interface IBackable +{ + public ViewModelBase Back(); +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/MainViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/MainViewModel.cs new file mode 100644 index 0000000..0f8c82a --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/MainViewModel.cs @@ -0,0 +1,60 @@ +namespace Tesses.CMS.Avalonia.ViewModels; + +using System.Collections.ObjectModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +using Tesses.CMS.Avalonia.Models; + +public partial class MainViewModel : ViewModelBase +{ + public MainViewModel(string title) + { + _pages.Add(new Page("Settings",()=>new SettingsPageViewModel(this))); + SelectedListItem = Pages.First(); + this.Title = title; + } + [ObservableProperty] + private bool _paneOpen=true; + [ObservableProperty] + private ViewModelBase _currentPage = new HomePageViewModel(); + + [ObservableProperty] + private ObservableCollection _pages=new ObservableCollection(){ + new Page("Home",()=>new HomePageViewModel()), + new Page("Favorites",()=>new FavoritesPageViewModel()), + new Page("Notifications",()=>new NotificationsPageViewModel()), + new Page("Downloads",()=>new DownloadsPageViewModel()), + + }; + + [ObservableProperty] + private Page? _selectedListItem; + [ObservableProperty] + private string _loginText="Login"; + + [ObservableProperty] + private string _title; + + partial void OnSelectedListItemChanged(Page? value) + { + if (value is null) return; + + + + + CurrentPage = value.ViewModel; + } + + [RelayCommand] + private void TogglePane() + { + PaneOpen = !PaneOpen; + } + [RelayCommand] + private void LoginAccount() + { + App.Log("Login button"); + } +} diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/NotificationsPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/NotificationsPageViewModel.cs new file mode 100644 index 0000000..b46182d --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/NotificationsPageViewModel.cs @@ -0,0 +1,8 @@ +namespace Tesses.CMS.Avalonia.ViewModels; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +public partial class NotificationsPageViewModel : ViewModelBase +{ + +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/SettingsPageViewModel.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/SettingsPageViewModel.cs new file mode 100644 index 0000000..b3b9bfe --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/SettingsPageViewModel.cs @@ -0,0 +1,50 @@ +namespace Tesses.CMS.Avalonia.ViewModels; + +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using CommunityToolkit.Mvvm.Messaging; +public partial class SettingsPageViewModel : ViewModelBase +{ + MainViewModel mvm; + public SettingsPageViewModel(MainViewModel mvm) + { + this.mvm = mvm; + } + + [RelayCommand] + private async Task Browse() + { + await App.Platform.BrowseForDownloadDirectoryAsync(); + if(App.Platform.PlatformUsesNormalPathsForDownload) + Path = App.Platform.Configuration.DownloadPath; + } + + [RelayCommand] + private async Task Save() + { + if(App.Platform.PlatformUsesNormalPathsForDownload) + App.Platform.Configuration.DownloadPath = Path; + + App.Platform.Configuration.ServerUrl = Url; + App.Platform.Configuration.Log = Log; + App.Platform.Configuration.CacheResources = CacheResources; + + await App.Platform.WriteConfigurationAsync(); + App.Client.RootUrl = App.Platform.Configuration.ServerUrl; + string title=App.GetApplicationTitle(); + mvm.Title = title; + if(App.Window != null) + App.Window.Title = title; + } + + [ObservableProperty] + private string _path = App.Platform.PlatformUsesNormalPathsForDownload ? App.Platform.Configuration.DownloadPath : ""; + + [ObservableProperty] + private string _url = App.Platform.Configuration.ServerUrl; + [ObservableProperty] + private bool _log = App.Platform.Configuration.Log; + [ObservableProperty] + private bool _cacheResources = App.Platform.Configuration.CacheResources; +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ViewModelBase.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..3b35673 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/ViewModels/ViewModelBase.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using ReactiveUI; + +namespace Tesses.CMS.Avalonia.ViewModels; + +public class ViewModelBase : ObservableObject +{ +} diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/DownloadsPageView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/DownloadsPageView.axaml new file mode 100644 index 0000000..9390bdf --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/DownloadsPageView.axaml @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/DownloadsPageView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/DownloadsPageView.axaml.cs new file mode 100644 index 0000000..eb4392a --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/DownloadsPageView.axaml.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Avalonia.Controls; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; + +namespace Tesses.CMS.Avalonia.Views; + +public partial class DownloadsPageView : UserControl +{ + public DownloadsPageView() + { + InitializeComponent(); + } + + +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/FavoritesPageView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/FavoritesPageView.axaml new file mode 100644 index 0000000..a732ec1 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/FavoritesPageView.axaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/FavoritesPageView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/FavoritesPageView.axaml.cs new file mode 100644 index 0000000..363ab4f --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/FavoritesPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views; + +public partial class FavoritesPageView : UserControl +{ + public FavoritesPageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeMovieListPageView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeMovieListPageView.axaml new file mode 100644 index 0000000..6ba60ba --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeMovieListPageView.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeMovieListPageView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeMovieListPageView.axaml.cs new file mode 100644 index 0000000..b96bd5f --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeMovieListPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views.HomePages; + +public partial class HomeMovieListPageView : UserControl +{ + public HomeMovieListPageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomePageView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomePageView.axaml new file mode 100644 index 0000000..7152497 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomePageView.axaml @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomePageView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomePageView.axaml.cs new file mode 100644 index 0000000..9f2462b --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomePageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views; + +public partial class HomePageView : UserControl +{ + public HomePageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserListPageView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserListPageView.axaml new file mode 100644 index 0000000..b70232d --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserListPageView.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserListPageView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserListPageView.axaml.cs new file mode 100644 index 0000000..5ee78fd --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserListPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views.HomePages; + +public partial class HomeUserListPageView : UserControl +{ + public HomeUserListPageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserPageView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserPageView.axaml new file mode 100644 index 0000000..9c281d9 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserPageView.axaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserPageView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserPageView.axaml.cs new file mode 100644 index 0000000..2b2bbb7 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/HomeUserPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views.HomePages; + +public partial class HomeUserPageView : UserControl +{ + public HomeUserPageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainView.axaml new file mode 100644 index 0000000..11671dd --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainView.axaml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainView.axaml.cs new file mode 100644 index 0000000..cf80bf0 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views; + +public partial class MainView : UserControl +{ + public MainView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainWindow.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainWindow.axaml new file mode 100644 index 0000000..d8c0736 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainWindow.axaml @@ -0,0 +1,12 @@ + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainWindow.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..4b39126 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/MainWindow.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views; + +public partial class MainWindow : Window +{ + public MainWindow() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/NotificationsPageView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/NotificationsPageView.axaml new file mode 100644 index 0000000..414b8ce --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/NotificationsPageView.axaml @@ -0,0 +1,15 @@ + + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/NotificationsPageView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/NotificationsPageView.axaml.cs new file mode 100644 index 0000000..771d3d9 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/NotificationsPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views; + +public partial class NotificationsPageView : UserControl +{ + public NotificationsPageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/SettingsPageView.axaml b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/SettingsPageView.axaml new file mode 100644 index 0000000..015febf --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/SettingsPageView.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + Cache Resources + Enable Logs + + + + + + diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/SettingsPageView.axaml.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/SettingsPageView.axaml.cs new file mode 100644 index 0000000..c543184 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/SettingsPageView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Tesses.CMS.Avalonia.Views; + +public partial class SettingsPageView : UserControl +{ + public SettingsPageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/VideoPlayerWrapper.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/VideoPlayerWrapper.cs new file mode 100644 index 0000000..2c61897 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Views/VideoPlayerWrapper.cs @@ -0,0 +1,41 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using LibVLCSharp.Shared; + +namespace Tesses.CMS.Avalonia.Views; + +public class VideoPlayerWrapper : Grid +{ + public VideoPlayerWrapper() + { + RowDefinitions.Add(new RowDefinition(GridLength.Star)); + ColumnDefinitions.Add(new ColumnDefinition(GridLength.Star)); + Player= App.Platform.CreatePlayer(); + this.Children.Add(Player); + } + + public Control Player {get;} + + public MediaPlayer? MediaPlayer + { + get=>App.Platform.GetMediaPlayer(Player); + set=>App.Platform.SetMediaPlayer(Player,value); + } + + + /// + /// MediaPlayer Data Bound property + /// + /// + /// Defines the property. + /// + public static readonly DirectProperty MediaPlayerProperty = + AvaloniaProperty.RegisterDirect( + nameof(MediaPlayer), + o => o.MediaPlayer, + (o, v) => o.MediaPlayer = v, + defaultBindingMode: BindingMode.TwoWay); + + +} \ No newline at end of file diff --git a/Tesses.CMS.Cli/Program.cs b/Tesses.CMS.Cli/Program.cs index c382861..4bb1a34 100644 --- a/Tesses.CMS.Cli/Program.cs +++ b/Tesses.CMS.Cli/Program.cs @@ -1,15 +1,222 @@ using Tesses.CMS.Client; +using CommandLine; +using System.Net; +using Newtonsoft.Json; -TessesCMSClient client = new TessesCMSClient("http://192.168.0.158:62444/"); +var prefs=Prefs.Create(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),"tcms.json")); -await foreach(var item in client.Movies.GetMoviesAsync("tesses")) + + + + + +var res =Parser.Default.ParseArguments(args); +res = await res.WithParsedAsync(MoviesCallback); +res = await res.WithParsedAsync(ShowsCallback); + +res = await res.WithParsedAsync(UsersCallback); + +res = await res.WithParsedAsync(EndpointCallback); + +res = res.WithParsed(EventsCallback); + +void EventsCallback(EventsOptions options) { - var res=await client.Movies.GetMovieContentMetadataAsync("tesses",item.Name); - Console.WriteLine(item.ProperName); - Console.WriteLine($"\t{res.PosterUrl}"); + Console.WriteLine("About to read events"); + using(var cms = new TessesCMSClient(prefs.Url)) + { + using(CancellationTokenSource src=new CancellationTokenSource()){ + cms.StartEvents((evt)=>{ + Console.WriteLine($"Type: {evt.Type}"); + Console.WriteLine($"Username: {evt.Username}"); + Console.WriteLine($"Userpropername: {evt.UserProperName}"); + Console.WriteLine($"Name: {evt.Name}"); + Console.WriteLine($"ProperName: {evt.ProperName}"); + bool hasBody = !string.IsNullOrWhiteSpace(evt.Body); + if(!string.IsNullOrWhiteSpace(evt.Description)) + { + Console.WriteLine("Description:"); + Console.WriteLine(evt.Description); + if(hasBody) + Console.WriteLine(); + } + if(hasBody) + { + Console.WriteLine("Body:"); + Console.WriteLine(evt.Body); + } + Console.WriteLine(); + },src.Token); + Console.ReadLine(); + src.Cancel(); + } + } } -//await client.Movies.CreateAsync("HolyLoop","Holy Loop"); -await client.Users.LogoutAsync(); \ No newline at end of file +async Task ShowsCallback(ShowsOptions options) +{ + throw new NotImplementedException(); +} + + +async Task UsersCallback(UsersOptions options) +{ + if(options.Login) + { + Console.Write("Email: "); + string? email=Console.ReadLine(); + if(string.IsNullOrWhiteSpace(email)) + { + Console.WriteLine("Email is empty"); + return; + } + string password = ReadLine.ReadPassword($"Password for {email}: "); + + + using(var clt = new TessesCMSClient(prefs.Url)) + { + var cookie = await clt.Users.GetCookieAsync(email,password); + if(cookie.Success) + { + if(!string.IsNullOrWhiteSpace(prefs.Session)) + { + await clt.Users.SetCookieAsync(prefs.Session); + await clt.Users.LogoutAsync(); + } + prefs.Session = cookie.Cookie; + prefs.Save(); + } + } + } + else if(options.Logout) + { + if(string.IsNullOrWhiteSpace(prefs.Session)) + { + Console.WriteLine("Not logged in"); + return; + } + using(var clt = new TessesCMSClient(prefs.Url)) + { + await clt.Users.SetCookieAsync(prefs.Session); + await clt.Users.LogoutAsync(); + prefs.Session=""; + prefs.Save(); + } + Console.WriteLine("Logged out"); + } + else { + using(var clt = new TessesCMSClient(prefs.Url)) + { + await foreach(var user in clt.Users.GetUsersAsync()) + { + Console.WriteLine($"Username: {user.Username}"); + Console.WriteLine($"Name: {user.ProperName}"); + Console.WriteLine("About:"); + Console.WriteLine(user.AboutMe); + Console.WriteLine(); + } + } + } +} + +async Task EndpointCallback(EndpointOptions options) +{ + if(string.IsNullOrWhiteSpace(options.Url)) + { + Console.WriteLine($"Current Endpoint: {prefs.Url}"); + } + else + { + prefs.Url = options.Url; + prefs.Save(); + Console.WriteLine($"Set Current Endpoint To: {options.Url}"); + } +} + + +async Task MoviesCallback(MoviesOptions options) +{ + if(options.List) + { + using(var clt = new TessesCMSClient(prefs.Url)) + { + await foreach(var movie in clt.Movies.GetMoviesAsync(options.Username)) + { + Console.WriteLine($"Title (ProperName): {movie.ProperName}"); + Console.WriteLine($"Name: {movie.Name}"); + Console.WriteLine($"Created: {movie.CreationTime}"); + Console.WriteLine($"Last Updated: {movie.LastUpdated}"); + Console.WriteLine("Description: "); + Console.WriteLine(movie.Description); + Console.WriteLine(); + } + } + + } +} +[Verb("endpoint",false,new string[]{"ep"},HelpText ="Change endpoint")] +internal class EndpointOptions +{ + [Value(0,Required =false,HelpText = "Main Page URL for placing request")] + public string Url {get;set;} = ""; +} +[Verb("users",false,HelpText = "User accounts")] +internal class UsersOptions +{ + [Option('l',"login",Required =false,HelpText = "Login")] + public bool Login {get;set;}=false; + + [Option('o',"logout",Required = false,HelpText = "Logout")] + public bool Logout {get;set;}=false; +} + +internal class ShowsOptions +{ +} +[Verb("movies",false,HelpText ="The movies")] +internal class MoviesOptions +{ + [Option('l',"list",HelpText = "List movies")] + public bool List {get;set;}=false; + + [Value(0,Required =true,HelpText = "Username")] + public string Username {get;set;}=""; +} + + +internal class Prefs +{ + [JsonProperty("url")] + public string Url {get;set;}="https://tessesstudios.com/"; + [JsonProperty("session")] + public string Session {get;set;}=""; + + [JsonIgnore] + string filename{get;set;}=""; + + + public static Prefs Create(string path) + { + if(File.Exists(path)) + { + var res=JsonConvert.DeserializeObject(File.ReadAllText(path)) ?? new Prefs(); + res.filename = path; + return res; + } + else + { + return new Prefs(){filename=path}; + } + } + + public void Save() + { + File.WriteAllText(filename,JsonConvert.SerializeObject(this)); + } +} +[Verb("events",false,new string[]{"evts"},HelpText ="Read server sent events (for debugging)")] +internal class EventsOptions +{ +} \ No newline at end of file diff --git a/Tesses.CMS.Cli/Tesses.CMS.Cli.csproj b/Tesses.CMS.Cli/Tesses.CMS.Cli.csproj index b03c821..90ea3ba 100644 --- a/Tesses.CMS.Cli/Tesses.CMS.Cli.csproj +++ b/Tesses.CMS.Cli/Tesses.CMS.Cli.csproj @@ -4,6 +4,11 @@ + + + + + Exe net7.0 diff --git a/Tesses.CMS.Client/Class1.cs b/Tesses.CMS.Client/Class1.cs index 3166003..8952a9d 100644 --- a/Tesses.CMS.Client/Class1.cs +++ b/Tesses.CMS.Client/Class1.cs @@ -2,17 +2,36 @@ using System.Collections; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Net; using System.Net.Http; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using System.Web; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; namespace Tesses.CMS.Client { - public class TessesCMSClient + public enum TessesCMSContentType { + Movie, + + Show, + + Album, + + MusicVideo, + + SoftwareProject, + + Other + } + public class TessesCMSClient : IDisposable + { + private static HttpClient CreateHttpClient() { HttpClientHandler httpClientHandler=new HttpClientHandler(); @@ -24,28 +43,88 @@ namespace Tesses.CMS.Client internal HttpClient client; internal string rooturl; public HttpClient Client => client; - public string RootUrl => $"{rooturl}/"; - public TessesCMSClient(string url,HttpClient client) + public string RootUrl { + get=>$"{rooturl}/"; + set { + rooturl = value.TrimEnd('/'); + } + } + bool ownsClient; + public TessesCMSClient(string url,HttpClient client,bool ownsClient) + { + this.ownsClient=ownsClient; this.client = client; rooturl = url.TrimEnd('/'); + } - public TessesCMSClient(HttpClient client) : this("https://tessesstudios.com/",client) - { - - } - - public TessesCMSClient(string url) : this(url,CreateHttpClient()) - { - - } - public TessesCMSClient() : this(CreateHttpClient()) + public TessesCMSClient(HttpClient client,bool ownsClient) : this("https://tessesstudios.com/",client,ownsClient) { } + + + public TessesCMSClient(string url) : this(url,CreateHttpClient(),true) + { + + } + public TessesCMSClient() : this(CreateHttpClient(),true) + { + + } + public async Task GetBrandingAsync() + { + return JsonConvert.DeserializeObject(await client.GetStringAsync($"{rooturl}/api/v1/Branding")); + } + public void StartEvents(Action evt,CancellationToken token=default) + { + Task.Run(async()=>{ + var clt=await client.GetSSEClientAsync($"{rooturl}/api/v1/Updates"); + await foreach(var item in clt.ReadEventsAsync(token)) + { + evt(item.ParseJson()); + } + }).Wait(0); + } + + public async Task CreateAsync(string urlname, string propername, string description, TessesCMSContentType type=TessesCMSContentType.Movie,CancellationToken token=default) + { + + + Dictionary kvp= new Dictionary + { + { "name", urlname }, + { "proper_name", propername }, + { "description", description }, + { "type", type.ToString().ToLower() } + }; + + + using(var res=await client.PostAsync($"{rooturl}/upload",new FormUrlEncodedContent(kvp),token)) + { + + return res.StatusCode != System.Net.HttpStatusCode.Unauthorized; + } + } + public async Task UploadFilePutAsync(string url,string file,CancellationToken token=default,IProgress progress=null) + { + using(var f = File.OpenRead(file)) + return await UploadFilePutAsync(url,f,token,progress); + } + public async Task UploadFilePutAsync(string url, Stream src,CancellationToken token=default,IProgress progress=null) + { + + var request = new HttpRequestMessage(HttpMethod.Put,url); + request.Content = new ProgressContent(src,progress); + + using(var res=await client.SendAsync(request,token)) + { + return res.StatusCode != System.Net.HttpStatusCode.Unauthorized; + } + } public async Task DownloadFileAsync(string url,string dest,CancellationToken token=default,IProgress progress=null) { - using(var f = File.Create(dest)) + using(var f = File.Open(dest,FileMode.OpenOrCreate,FileAccess.Write)) await DownloadFileAsync(url,f,token,progress); } public async Task DownloadFileAsync(string url,Stream dest,CancellationToken token=default,IProgress progress=null) @@ -83,11 +162,88 @@ namespace Tesses.CMS.Client } while(read>0); resp.Dispose(); } - + public ShowClient Shows => new ShowClient(this); public MovieClient Movies => new MovieClient(this); public UserClient Users => new UserClient(this); + + public void Dispose() + { + if(this.ownsClient) + this.client.Dispose(); + } } + + public class Branding + { + [JsonProperty("title")] + public string Title {get;set;}=""; + } + + [JsonConverter(typeof(StringEnumConverter),typeof(SnakeCaseNamingStrategy))] + public enum EventType + { + MovieCreate, + MovieUpdate, + ShowCreate, + ShowUpdate + } + public class CMSEvent + { + [JsonProperty("eventtype")] + public EventType Type {get;set;} + + [JsonProperty("username")] + public string Username {get;set;}=""; + [JsonProperty("userpropername")] + public string UserProperName {get;set;}=""; + + [JsonProperty("name")] + public string Name {get;set;}=""; + + [JsonProperty("propername")] + public string ProperName {get;set;}=""; + + [JsonProperty("description")] + public string Description {get;set;}=""; + + [JsonProperty("body")] + public string Body {get;set;}=""; + } + + internal class ProgressContent : HttpContent + { + private Stream src; + private IProgress progress; + + public ProgressContent(Stream src, IProgress progress) + { + this.src = src; + this.progress = progress; + } + + protected override async Task SerializeToStreamAsync(Stream stream, TransportContext context) + { + int read = 0; + byte[] buffer=new byte[1024]; + double offset=0; + double length = src.Length; + do { + read = await src.ReadAsync(buffer,0,buffer.Length); + offset += read; + await stream.WriteAsync(buffer,0,read); + if(length != 0) + progress?.Report(offset / length); + } while(read != 0); + } + + protected override bool TryComputeLength(out long length) + { + length = src.Length; + return true; + } + } + public class UserClient { TessesCMSClient client; @@ -95,21 +251,115 @@ namespace Tesses.CMS.Client { this.client = client; } + public async Task LogoutAsync() { - await client.client.GetStringAsync($"{client.rooturl}/logout"); + (await client.client.GetAsync("/logout")).Dispose(); } - public async Task LoginAsync(string email,string password) + + + public async Task CreateTokenAsync(string email,string password) { Dictionary us=new Dictionary(); us.Add("email",email); us.Add("password",password); - using(var res=await client.client.PostAsync($"{client.rooturl}/login", new FormUrlEncodedContent(us))) - return res.StatusCode != System.Net.HttpStatusCode.Unauthorized; + us.Add("type","json"); + using(var res=await client.client.PostAsync($"{client.rooturl}/api/v1/Login", new FormUrlEncodedContent(us))) + + return JsonConvert.DeserializeObject( await res.Content.ReadAsStringAsync()); + + } + + public string LoginToken + { + get { + if(client.client.DefaultRequestHeaders.Contains("Authorization")) + return client.client.DefaultRequestHeaders.Authorization.Parameter; + return ""; + } + set{ + if(string.IsNullOrWhiteSpace(value)) + if(client.client.DefaultRequestHeaders.Contains("Authorization")) + client.client.DefaultRequestHeaders.Remove("Authorization"); + else + client.client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer",value); + } + } + + public async IAsyncEnumerable GetUsersAsync() + { + foreach(var user in JsonConvert.DeserializeObject>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetPublicUsers"))) + { + yield return user; + } } } - public class MovieClient + public class LoginToken + { + [JsonProperty("success")] + public bool Success {get;set;}=false; + [JsonProperty("cookie")] + public string Cookie {get;set;}=""; + } + + public class ShowClient + { + TessesCMSClient client; + internal ShowClient(TessesCMSClient client) + { + this.client = client; + } + + public async IAsyncEnumerable GetShowsAsync(string user) + { + foreach(var item in JsonConvert.DeserializeObject>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetShows?user={HttpUtility.UrlEncode(user)}"))) + { + yield return item; + } + } + public async IAsyncEnumerable GetSeasonsAsync(string user,string show) + { + foreach(var item in JsonConvert.DeserializeObject>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetSeasons?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(show)}"))) + { + yield return item; + } + } + public async IAsyncEnumerable GetEpisodesAsync(string user,string show,int season) + { + foreach(var item in JsonConvert.DeserializeObject>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetEpisodes?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(show)}&season={season}"))) + { + yield return item; + } + } + public async IAsyncEnumerable GetEpisodesAsync(string user,string show) + { + await foreach(var season in GetSeasonsAsync(user,show)) + { + List episodes=new List(); + await foreach(var episode in GetEpisodesAsync(user,show,season.SeasonNumber)) + { + episodes.Add(episode); + } + yield return new SeasonWithEpisodes(season,episodes); + } + } + public async IAsyncEnumerable GetEpisodesAsync(string user) + { + await foreach(var show in GetShowsAsync(user)) + { + List seasons=new List(); + await foreach(var episode in GetEpisodesAsync(user,show.Name)) + { + seasons.Add(episode); + } + yield return new ShowWithSeasonsAndEpisodes(show,seasons); + } + } + + } + + public class MovieClient { TessesCMSClient client; internal MovieClient(TessesCMSClient client) @@ -118,7 +368,7 @@ namespace Tesses.CMS.Client } public async IAsyncEnumerable GetMoviesAsync(string user) { - foreach(var item in JsonConvert.DeserializeObject>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetMovies?user={HttpUtility.UrlDecode(user)}"))) + foreach(var item in JsonConvert.DeserializeObject>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetMovies?user={HttpUtility.UrlEncode(user)}"))) { yield return item; } @@ -127,14 +377,213 @@ namespace Tesses.CMS.Client { return JsonConvert.DeserializeObject(await client.client.GetStringAsync($"{client.rooturl.TrimEnd('/')}/api/v1/MovieFile?movie={movie}&user={user}&type=json")); } + public async Task UploadExtrasAsync(string user, string movie, string dir, CancellationToken token=default,IProgress progress=null) + { + List<(string localPath,string removePath)> items=new List<(string localPath, string removePath)>(); + async Task EnumerateDir(string local, string remote) + { + if(!string.IsNullOrWhiteSpace(remote)) + await CreateExtraDirectoryAsync(user,movie,remote,token); + + foreach(var dir in Directory.EnumerateDirectories(local)) + { + await EnumerateDir(dir,$"{remote}/{Path.GetFileName(dir)}"); + } + foreach(var file in Directory.EnumerateFiles(local)) + { + string name = $"{remote}/{Path.GetFileName(file)}"; + items.Add((file,name)); + } + } + + await EnumerateDir(dir,""); + for(int i = 0;i(e=>{ + double j = i + e; + progress?.Report(j / items.Count); + })); + + } + + } + public async Task UploadExtraAsync(string user, string movie,string extra, string src,CancellationToken token=default,IProgress progress=null) + { + string url = $"{client.rooturl}/api/v1/MovieExtra?user={user}&movie={movie}&extra={HttpUtility.UrlEncode(extra.TrimStart('/'))}"; + + + return await client.UploadFilePutAsync(url,src,token,progress); + } + + public async Task UploadExtraAsync(string user, string movie,string extra, Stream src,CancellationToken token=default,IProgress progress=null) + { + string url = $"{client.rooturl}/api/v1/MovieExtra?user={user}&movie={movie}&extra={HttpUtility.UrlEncode(extra.TrimStart('/'))}"; + + return await client.UploadFilePutAsync(url,src,token,progress); + } + + public async Task UploadMovieAsync(string user, string movie, string src,CancellationToken token=default,IProgress progress=null) + { + string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=movie"; + + return await client.UploadFilePutAsync(url,src,token,progress); + } + + public async Task UploadMovieAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress progress=null) + { + string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=movie"; + + return await client.UploadFilePutAsync(url,src,token,progress); + } + + public async Task UploadPosterAsync(string user, string movie, string src,CancellationToken token=default,IProgress progress=null) + { + string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=poster"; + + return await client.UploadFilePutAsync(url,src,token,progress); + } + + public async Task UploadPosterAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress progress=null) + { + string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=poster"; + + return await client.UploadFilePutAsync(url,src,token,progress); + } + public async Task UploadThumbnailAsync(string user, string movie, string src,CancellationToken token=default,IProgress progress=null) + { + string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=thumbnail"; + + return await client.UploadFilePutAsync(url,src,token,progress); + } + + public async Task UploadThumbnailAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress progress=null) + { + string url = $"{client.rooturl}/api/v1/MovieFile?user={user}&movie={movie}&type=thumbnail"; + + return await client.UploadFilePutAsync(url,src,token,progress); + } + + public async Task CreateExtraDirectoryAsync(string user,string movie, string extraDir,CancellationToken token=default) + { + string parent=""; + string name=Path.GetFileName(extraDir); + try{ + parent=Path.GetDirectoryName(extraDir.TrimStart('/')); + } + catch(Exception) + { + parent=""; + } + + Dictionary kvp= new Dictionary + { + { "name", name }, + { "parent", parent }, + + }; + + + using(var res=await client.client.PostAsync($"{client.rooturl}/user/{user}/movie/{movie}/mkdir",new FormUrlEncodedContent(kvp),token)) + { + + return res.StatusCode != System.Net.HttpStatusCode.Unauthorized; + } + } + public async Task DownloadMovieAsync(string user,string movie,string dest,CancellationToken token=default,IProgress progress=null) { - await client.DownloadFileAsync($"{client.rooturl.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress); + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress); } public async Task DownloadMovieAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress progress=null) { - await client.DownloadFileAsync($"{client.rooturl.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress); + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress); + } + + + public async Task DownloadThumbnailAsync(string user,string movie,string dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/thumbnail.jpg",dest,token,progress); + } + public async Task DownloadThumbnailAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/thumbnail.jpg",dest,token,progress); + } + + public async Task DownloadPosterAsync(string user,string movie,string dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/poster.jpg",dest,token,progress); + } + public async Task DownloadPosterAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/poster.jpg",dest,token,progress); + } + public async Task DownloadExtraAsync(string user,string movie,string path,string dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/extras/{HttpUtility.UrlPathEncode(path.TrimStart('/'))}",dest,token,progress); + } + public async Task DownloadExtraAsync(string user,string movie,string path,Stream dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/extras/{HttpUtility.UrlPathEncode(path.TrimStart('/'))}",dest,token,progress); + } + + public async Task DownloadTorrentAsync(string user,string movie,string dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}.torrent",dest,token,progress); + } + public async Task DownloadTorrentAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}.torrent",dest,token,progress); + } + + + public async Task DownloadTorrentWithExtrasAsync(string user,string movie,string dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}_withextras.torrent",dest,token,progress); + } + public async Task DownloadTorrentWithExtrasAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress progress=null) + { + await client.DownloadFileAsync($"{client.rooturl}/content/{user}/movie/{movie}/{movie}_withextras.torrent",dest,token,progress); + } + + public async Task DownloadExtrasAsync(string user,string movie, string dir, CancellationToken token=default,IProgress progress=null) + { + var r = await GetMovieContentMetadataAsync(user,movie); + List<(string inDir,string outDir)> items=new List<(string inDir, string outDir)>(); + void DownloadExtraDir(List files,string outDir,string inDir) + { + Directory.CreateDirectory(outDir); + foreach(var item in files) + { + if(item.IsDir) + { + DownloadExtraDir(item.Items,Path.Combine(outDir,item.Name),$"{inDir}/{item.Name}"); + } + else + { + items.Add(($"{inDir}/{item.Name}",Path.Combine(outDir,item.Name))); + } + } + } + + + + DownloadExtraDir(r.ExtraStreams,dir,""); + for(int i = 0;i(e=>{ + double j = i + e; + progress?.Report(j / items.Count); + })); + + } + } + + + public async Task CreateAsync(string urlname, string propername, string description) + { + return await client.CreateAsync(urlname,propername,description); } } } + diff --git a/Tesses.CMS.Client/Episode.cs b/Tesses.CMS.Client/Episode.cs new file mode 100644 index 0000000..f11e54a --- /dev/null +++ b/Tesses.CMS.Client/Episode.cs @@ -0,0 +1,25 @@ +using System; +using Newtonsoft.Json; + +namespace Tesses.CMS.Client +{ + public class Episode + { + [JsonProperty("proper_name")] + public string ProperName {get;set;}=""; + [JsonProperty("name")] + public string Name {get;set;}=""; + [JsonProperty("season_number")] + public int SeasonNumber {get;set;} + [JsonProperty("episode_number")] + public int EpisodeNumber {get;set;} + [JsonProperty("episode_name")] + public string EpisodeName {get;set;}=""; + [JsonProperty("creation_time")] + public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] + public DateTime LastUpdated {get;set;} + [JsonProperty("description")] + public string Description {get;set;}=""; + } +} \ No newline at end of file diff --git a/Tesses.CMS.Client/Movie.cs b/Tesses.CMS.Client/Movie.cs index a9d8e1e..f3d94f4 100644 --- a/Tesses.CMS.Client/Movie.cs +++ b/Tesses.CMS.Client/Movie.cs @@ -6,20 +6,33 @@ namespace Tesses.CMS.Client { public class MovieContentMetaData { + [JsonProperty("has_movie_torrent")] + public bool HasMovieTorrent{get;set;} [JsonProperty("movie_torrent_url")] public string MovieTorrentUrl {get;set;} - + [JsonProperty("has_movie_with_extras_torrent")] + public bool HasMovieWithExtrasTorrent{get;set;} [JsonProperty("movie_with_extras_torrent_url")] public string MovieWithExtrasTorrentUrl {get;set;} + [JsonProperty("has_browser_stream")] + public bool HasBrowserStream {get;set;} + [JsonProperty("has_download_stream")] + public bool HasDownloadStream {get;set;} [JsonProperty("browser_stream")] public string BrowserStream {get;set;} + [JsonProperty("download_stream")] public string DownloadStream {get;set;} + [JsonProperty("has_poster")] + public bool HasPoster {get;set;} [JsonProperty("poster_url")] public string PosterUrl {get;set;} + + [JsonProperty("has_thumbnail")] + public bool HasThumbnail {get;set;} [JsonProperty("thumbnail_url")] @@ -56,14 +69,15 @@ namespace Tesses.CMS.Client public class Movie { + [JsonProperty("proper_name")] public string ProperName {get;set;} - + [JsonProperty("name")] public string Name {get;set;} - + [JsonProperty("creation_time")] public DateTime CreationTime {get;set;} - + [JsonProperty("last_updated_time")] public DateTime LastUpdated {get;set;} - + [JsonProperty("description")] public string Description {get;set;} } } \ No newline at end of file diff --git a/Tesses.CMS.Client/SSEClient.cs b/Tesses.CMS.Client/SSEClient.cs new file mode 100644 index 0000000..e6432cb --- /dev/null +++ b/Tesses.CMS.Client/SSEClient.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Tesses.CMS.Client +{ + public static class SSEExtensions +{ + public static async Task GetSSEClientAsync(this HttpClient clt,string url) + { + var strm=await clt.GetStreamAsync(url); + return new SSEClient(strm); + } +} + +public class SSEEvent +{ + public SSEEvent(string data) + { + Data=data; + } + public string Data {get;set;} + public T ParseJson() + { + return JsonConvert.DeserializeObject(Data); + } +} +public class SSEClient +{ + Stream strm; + + public SSEClient(Stream strm) + { + this.strm=strm; + } + + + public async IAsyncEnumerable ReadEventsAsync([EnumeratorCancellation]CancellationToken token=default(CancellationToken)) + { + + using(var sr = new StreamReader(strm)){ + token.Register(()=>{ + sr.Dispose(); + }); + + while(!token.IsCancellationRequested) + { + + string text=""; + try{ + text=await sr.ReadLineAsync(); + }catch(Exception ex) + { + _=ex; + } + if(!string.IsNullOrWhiteSpace(text)) + yield return new SSEEvent(text.Substring(6)); + } + } + + } + +} +} \ No newline at end of file diff --git a/Tesses.CMS.Client/Season.cs b/Tesses.CMS.Client/Season.cs new file mode 100644 index 0000000..bbee93b --- /dev/null +++ b/Tesses.CMS.Client/Season.cs @@ -0,0 +1,22 @@ +using System; +using Newtonsoft.Json; + +namespace Tesses.CMS.Client +{ + public class Season + { + [JsonProperty("proper_name")] + public string ProperName {get;set;}=""; + [JsonProperty("name")] + public string Name {get;set;}=""; + [JsonProperty("season_number")] + public int SeasonNumber {get;set;} + + [JsonProperty("creation_time")] + public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] + public DateTime LastUpdated {get;set;} + [JsonProperty("description")] + public string Description {get;set;}=""; + } +} \ No newline at end of file diff --git a/Tesses.CMS.Client/SeasonWithEpisodes.cs b/Tesses.CMS.Client/SeasonWithEpisodes.cs new file mode 100644 index 0000000..ca683fb --- /dev/null +++ b/Tesses.CMS.Client/SeasonWithEpisodes.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace Tesses.CMS.Client +{ + public class SeasonWithEpisodes + { + public SeasonWithEpisodes(Season season,List episodes) + { + ProperName = season.ProperName; + Name = season.Name; + SeasonNumber = season.SeasonNumber; + CreationTime = season.CreationTime; + LastUpdated = season.LastUpdated; + Description = season.Description; + Episodes = episodes; + } + public string ProperName {get;set;}=""; + + public string Name {get;set;}=""; + public int SeasonNumber {get;set;} + + + public DateTime CreationTime {get;set;} + public DateTime LastUpdated {get;set;} + + public string Description {get;set;}=""; + + public List Episodes {get;set;} + } +} \ No newline at end of file diff --git a/Tesses.CMS.Client/Show.cs b/Tesses.CMS.Client/Show.cs new file mode 100644 index 0000000..d4a7a50 --- /dev/null +++ b/Tesses.CMS.Client/Show.cs @@ -0,0 +1,20 @@ +using System; +using Newtonsoft.Json; + +namespace Tesses.CMS.Client +{ + public class Show + { + [JsonProperty("proper_name")] + public string ProperName {get;set;}=""; + [JsonProperty("name")] + public string Name {get;set;}=""; + + [JsonProperty("creation_time")] + public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] + public DateTime LastUpdated {get;set;} + [JsonProperty("description")] + public string Description {get;set;}=""; + } +} \ No newline at end of file diff --git a/Tesses.CMS.Client/ShowWithSeasonsAndEpisodes.cs b/Tesses.CMS.Client/ShowWithSeasonsAndEpisodes.cs new file mode 100644 index 0000000..b87d5a7 --- /dev/null +++ b/Tesses.CMS.Client/ShowWithSeasonsAndEpisodes.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; + +namespace Tesses.CMS.Client +{ + public class ShowWithSeasonsAndEpisodes + { + public ShowWithSeasonsAndEpisodes(Show show,List seasons) + { + ProperName = show.ProperName; + Name = show.Name; + CreationTime = show.CreationTime; + LastUpdated = show.LastUpdated; + Description = show.Description; + Seasons = seasons; + } + + public string ProperName {get;set;}=""; + + public string Name {get;set;}=""; + + + public DateTime CreationTime {get;set;} + public DateTime LastUpdated {get;set;} + + public string Description {get;set;}=""; + public List Seasons {get;set;} + } +} \ No newline at end of file diff --git a/Tesses.CMS.Client/UserAccount.cs b/Tesses.CMS.Client/UserAccount.cs new file mode 100644 index 0000000..1b77896 --- /dev/null +++ b/Tesses.CMS.Client/UserAccount.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace Tesses.CMS.Client +{ + public class UserAccount + { + [JsonProperty("username")] + public string Username {get;set;}=""; + [JsonProperty("proper_name")] + public string ProperName {get;set;}=""; + [JsonProperty("about_me")] + public string AboutMe {get;set;}=""; + } +} \ No newline at end of file diff --git a/Tesses.CMS.Providers.Dapper/Class1.cs b/Tesses.CMS.Providers.Dapper/Class1.cs index 24883ca..e5b76cd 100644 --- a/Tesses.CMS.Providers.Dapper/Class1.cs +++ b/Tesses.CMS.Providers.Dapper/Class1.cs @@ -46,6 +46,11 @@ namespace Tesses.CMS.Providers return false; } + public Episode CreateEpisode(string user, string show, int season, int episode, string episodename, string properName, string description) + { + throw new NotImplementedException(); + } + public Movie CreateMovie(string user, string movie, string properName, string description) { var userId=GetUserAccount(user).Id; @@ -59,11 +64,21 @@ namespace Tesses.CMS.Providers return GetMovie(user,movie); } + public Season CreateSeason(string user, string show, int season, string properName, string description) + { + throw new NotImplementedException(); + } + public void CreateSession(string session, long account) { con.Execute("INSERT INTO Sessions(Session,Account) values (@session,@account);",new{session,account}); } + public Show CreateShow(string user, string show, string properName, string description) + { + throw new NotImplementedException(); + } + public void CreateUser(CMSConfiguration configuration, string user, string properName, string email, string password) { bool first=con.QueryFirstOrDefault("SELECT * FROM Users LIMIT 0, 1;") == null; @@ -99,6 +114,16 @@ namespace Tesses.CMS.Providers con.Execute("DELETE FROM VerificationCodes WHERE Session = @code;",new{code}); } + public int EpisodeCount(string user, string show, int season) + { + throw new NotImplementedException(); + } + + public Episode GetEpisode(string user, string show, int season, int episode) + { + throw new NotImplementedException(); + } + public UserAccount GetFirstUser() { return con.QueryFirstOrDefault("SELECT * FROM Users LIMIT 0, 1;")?.Account; @@ -131,11 +156,26 @@ namespace Tesses.CMS.Providers } } + public Season GetSeason(string user, string show, int season) + { + throw new NotImplementedException(); + } + public long? GetSession(string session) { return con.QueryFirstOrDefault("SELECT * FROM Sessions WHERE Session = @session;",new{session})?.Id; } + public Show GetShow(string user, string show) + { + throw new NotImplementedException(); + } + + public IEnumerable GetShows(string user) + { + throw new NotImplementedException(); + } + public UserAccount GetUserAccount(string user) { return con.QueryFirstOrDefault("SELECT * FROM Users WHERE Username=@user;",new{user})?.Account; @@ -170,12 +210,32 @@ namespace Tesses.CMS.Providers return null; } + public int SeasonCount(string user, string show) + { + throw new NotImplementedException(); + } + + public void UpdateEpisode(Episode episode) + { + throw new NotImplementedException(); + } + public void UpdateMovie(Movie movie) { DapperMovie dapperMovie=new DapperMovie(movie); con.Execute("UPDATE Users set movie = @movie WHERE Id = @id;",new{movie=dapperMovie,id=movie.Id}); } + public void UpdateSeason(Season season) + { + throw new NotImplementedException(); + } + + public void UpdateShow(Show show) + { + throw new NotImplementedException(); + } + public void UpdateUser(UserAccount account) { DapperUserAccount account1=new DapperUserAccount(account); diff --git a/Tesses.CMS.Providers.LiteDb/Class1.cs b/Tesses.CMS.Providers.LiteDb/Class1.cs index 96bc27e..aa54f97 100644 --- a/Tesses.CMS.Providers.LiteDb/Class1.cs +++ b/Tesses.CMS.Providers.LiteDb/Class1.cs @@ -18,17 +18,13 @@ public class LiteDBContentProvider : IContentProvider { var userId=GetUserAccount(user).Id; - Movie _movie = new Movie(){UserId = userId,Name = movie,ProperName=properName,Description = description}; - _movie.Id=Movies.Insert(_movie); - return _movie; + return CreateMovie(userId,movie,properName,description); } public Show CreateShow(string user, string show, string properName, string description) { var userId=GetUserAccount(user).Id; - Show _show = new Show(){UserId = userId,Name = show,ProperName=properName,Description = description}; - _show.Id=Shows.Insert(_show); - return _show; + return CreateShow(userId,show,properName,description); } private ILiteCollection UserAccounts => db.GetCollection("users"); private ILiteCollection Movies => db.GetCollection("movies"); @@ -39,7 +35,7 @@ public class LiteDBContentProvider : IContentProvider private ILiteCollection Sessions => db.GetCollection("sessions"); private ILiteCollection VerificationCodes => db.GetCollection("verificationcodes"); - + private ILiteCollection Albums => db.GetCollection("albums"); public UserAccount GetFirstUser() { return GetUsers().First(); @@ -105,10 +101,12 @@ public class LiteDBContentProvider : IContentProvider public void UpdateMovie(Movie movie) { + movie.LastUpdated = DateTime.Now; Movies.Update(movie); } public void UpdateShow(Show show) { + show.LastUpdated = DateTime.Now; Shows.Update(show); } @@ -210,13 +208,7 @@ public class LiteDBContentProvider : IContentProvider var myShow = GetShow(user,show); var userId = myShow.UserId; var showId = myShow.Id; - int seasonLargest=0; - foreach(var item in Seasons.Find(e=>e.ShowId==showId && e.UserId == userId)) - { - if(item.SeasonNumber > seasonLargest) - seasonLargest = item.SeasonNumber; - } - return seasonLargest; + return SeasonCount(userId,showId); } public Season GetSeason(string user, string show, int season) @@ -224,7 +216,7 @@ public class LiteDBContentProvider : IContentProvider var myShow = GetShow(user,show); var userId = myShow.UserId; var showId = myShow.Id; - return Seasons.FindOne(e=>e.ShowId == showId && e.UserId == userId && e.SeasonNumber == season); + return GetSeason(userId,showId,season); } public Season CreateSeason(string user, string show, int season, string properName, string description) @@ -232,9 +224,7 @@ public class LiteDBContentProvider : IContentProvider var myShow = GetShow(user,show); var userId = myShow.UserId; var showId = myShow.Id; - Season _season = new Season(){UserId = userId,ShowId = showId,ProperName=properName,Description = description, SeasonNumber=season}; - _season.Id=Seasons.Insert(_season); - return _season; + return CreateSeason(userId,showId,season,properName,description); } public int EpisodeCount(string user, string show, int season) @@ -242,22 +232,16 @@ public class LiteDBContentProvider : IContentProvider var myShow = GetShow(user,show); var userId = myShow.UserId; var showId = myShow.Id; - int episodeLargest=0; - foreach(var item in Episodes.Find(e=>e.ShowId==showId && e.UserId == userId && e.SeasonNumber == season)) - { - if(item.EpisodeNumber > episodeLargest) - episodeLargest = item.EpisodeNumber; - } - return episodeLargest; + return EpisodeCount(userId,showId,season); } public Episode GetEpisode(string user, string show, int season, int episode) { var myShow = GetShow(user,show); + var userId = myShow.UserId; var showId = myShow.Id; - return Episodes.FindOne(e=>e.ShowId == showId && e.UserId == userId && e.SeasonNumber == season && e.EpisodeNumber == episode); - + return GetEpisode(userId,showId,season,episode); } public Episode CreateEpisode(string user, string show, int season, int episode, string episodename, string properName, string description) @@ -265,19 +249,136 @@ public class LiteDBContentProvider : IContentProvider var myShow = GetShow(user,show); var userId = myShow.UserId; var showId = myShow.Id; - Episode _episode = new Episode(){UserId = userId,ShowId = showId,ProperName=properName,Description = description, SeasonNumber=season, EpisodeNumber = episode,EpisodeName=episodename}; - _episode.Id=Episodes.Insert(_episode); - return _episode; + return CreateEpisode(userId,showId,season,episode,episodename,properName,description); } public void UpdateEpisode(Episode episode) { + episode.LastUpdated = DateTime.Now; Episodes.Update(episode); } public void UpdateSeason(Season season) { + season.LastUpdated = DateTime.Now; Seasons.Update(season); } + + public Album CreateAlbum(string user, string album, string properName, string description) + { + var userId=GetUserAccount(user).Id; + + return CreateAlbum(userId,album,properName,description); + } + + public Album GetAlbum(string user, string album) + { + var userId=GetUserAccount(user).Id; + return GetAlbum(userId,album); + } + + public void UpdateAlbum(Album album) + { + album.LastUpdated = DateTime.Now; + Albums.Update(album); + } + + public IEnumerable GetAlbums(string user) + { + return GetAlbums(GetUserAccount(user).Id); + } + + public IEnumerable GetAlbums(long user) + { + return Albums.Find(e=>e.UserId == user); + } + + public Movie CreateMovie(long user, string movie, string properName, string description) + { + Movie _movie = new Movie(){UserId = user,Name = movie,ProperName=properName,Description = description}; + _movie.CreationTime = DateTime.Now; + _movie.LastUpdated = DateTime.Now; + _movie.Id=Movies.Insert(_movie); + return _movie; + } + + public Album CreateAlbum(long user, string album, string properName, string description) + { + Album _album = new Album(){UserId = user,Name = album,ProperName=properName,Description = description}; + _album.CreationTime = DateTime.Now; + _album.LastUpdated = DateTime.Now; + _album.Id=Albums.Insert(_album); + return _album; + } + + public Show CreateShow(long user, string show, string properName, string description) + { + Show _show = new Show(){UserId = user,Name = show,ProperName=properName,Description = description}; + _show.CreationTime = DateTime.Now; + _show.LastUpdated = DateTime.Now; + _show.Id=Shows.Insert(_show); + return _show; + } + + public Show GetShow(long user, long show) + { + return Shows.FindOne(e=>e.Id == show && e.UserId == user); + } + + public int SeasonCount(long user, long show) + { + int seasonLargest=0; + foreach(var item in Seasons.Find(e=>e.ShowId==show && e.UserId == user)) + { + if(item.SeasonNumber > seasonLargest) + seasonLargest = item.SeasonNumber; + } + return seasonLargest; + } + + public Season GetSeason(long user, long show, int season) + { + return Seasons.FindOne(e=>e.ShowId == show && e.UserId == user && e.SeasonNumber == season); + } + + public Season CreateSeason(long user, long show, int season, string properName, string description) + { + Season _season = new Season(){UserId = user,ShowId = show,ProperName=properName,Description = description, SeasonNumber=season}; + _season.CreationTime = DateTime.Now; + _season.LastUpdated = DateTime.Now; + _season.Id=Seasons.Insert(_season); + return _season; + } + + public int EpisodeCount(long user, long show, int season) + { + int episodeLargest=0; + foreach(var item in Episodes.Find(e=>e.ShowId==show && e.UserId == user && e.SeasonNumber == season)) + { + if(item.EpisodeNumber > episodeLargest) + episodeLargest = item.EpisodeNumber; + } + return episodeLargest; + } + + public Episode GetEpisode(long user, long show, int season, int episode) + { + return Episodes.FindOne(e=>e.ShowId == show && e.UserId == user && e.SeasonNumber == season && e.EpisodeNumber == episode); + + } + + public Episode CreateEpisode(long user, long show, int season, int episode, string episodename, string properName, string description) + { + Episode _episode = new Episode(){UserId = user,ShowId = show,ProperName=properName,Description = description, SeasonNumber=season, EpisodeNumber = episode,EpisodeName=episodename}; + _episode.CreationTime = DateTime.Now; + _episode.LastUpdated = DateTime.Now; + _episode.Id=Episodes.Insert(_episode); + return _episode; + } + + public Album GetAlbum(long user, string album) + { + return Albums.FindOne(e=>e.Name == album && e.UserId == user); + } } } diff --git a/Tesses.CMS.Server/Tesses.CMS.Server.csproj b/Tesses.CMS.Server/Tesses.CMS.Server.csproj index 40a5032..2d6cffc 100644 --- a/Tesses.CMS.Server/Tesses.CMS.Server.csproj +++ b/Tesses.CMS.Server/Tesses.CMS.Server.csproj @@ -2,7 +2,7 @@ Exe - net7.0 + net8.0 enable enable diff --git a/Tesses.CMS/Album.cs b/Tesses.CMS/Album.cs new file mode 100644 index 0000000..8f493fb --- /dev/null +++ b/Tesses.CMS/Album.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Tesses.CMS +{ + public class Album + { + [JsonIgnore] + public long Id {get;set;} + [JsonProperty("proper_name")] + + public string ProperName {get;set;} + [JsonProperty("name")] + public string Name {get;set;} + [JsonProperty("album_artist")] + public string AlbumArtist {get;set;} = "Unknown Artist"; + [JsonProperty("tracks")] + public List Tracks {get;set;}=new List(); + [JsonProperty("year")] + public int Year {get;set;}=DateTime.Now.Year; + + [JsonIgnore] + public long UserId {get;set;} + [JsonProperty("creation_time")] + public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] + public DateTime LastUpdated {get;set;} + [JsonProperty("description")] + public string Description {get;set;} + + public object Scriban(string thumbnail) + { + return new { + Proper = System.Web.HttpUtility.HtmlEncode( ProperName), + Name = System.Web.HttpUtility.HtmlEncode(Name), + Description = System.Web.HttpUtility.HtmlEncode(Description), + Thumbnail = thumbnail + }; + } + + } +} \ No newline at end of file diff --git a/Tesses.CMS/AssetProvier.cs b/Tesses.CMS/AssetProvier.cs index 2021d7b..dd4d778 100644 --- a/Tesses.CMS/AssetProvier.cs +++ b/Tesses.CMS/AssetProvier.cs @@ -25,7 +25,7 @@ namespace Tesses.CMS public override async Task GetAsync(ServerContext ctx) { try{ - await ctx.SendTextAsync(await ReadAllTextAsync(ctx.UrlPath),HeyRed.Mime.MimeTypesMap.GetMimeType(ctx.UrlPath)); + await ctx.SendStreamAsync(OpenRead(ctx.UrlPath),HeyRed.Mime.MimeTypesMap.GetMimeType(ctx.UrlPath)); }catch(ArgumentNullException ex) { _=ex; diff --git a/Tesses.CMS/Assets/AddMovie.html b/Tesses.CMS/Assets/AddMovie.html deleted file mode 100644 index e69de29..0000000 diff --git a/Tesses.CMS/Assets/AlbumPage.html b/Tesses.CMS/Assets/AlbumPage.html new file mode 100644 index 0000000..f362af1 --- /dev/null +++ b/Tesses.CMS/Assets/AlbumPage.html @@ -0,0 +1,50 @@ +
+
+ {{albumproper}} +

{{albumproper}}

+

{{userproper}}

+
+ + Listen Online + {{if editable}} + Edit + {{end}} + {{if extrasexists}} + Extras + {{end}} + {{if torrentexists}} + Torrent + {{end}} + {{if torrentwextraexists}} + Torrent With Extras + {{end}} + {{if editable}} +
+
+ + +
+ +
+ {{end}} +
+ Note to Touchscreen users: Touch and hold seekbar to set position in song + +
+
+
+

+ +

+
+
+
+

{{albumdescription}}

+
+
+
+
+ +
\ No newline at end of file diff --git a/Tesses.CMS/Assets/AlbumsPage.html b/Tesses.CMS/Assets/AlbumsPage.html new file mode 100644 index 0000000..a1a0a77 --- /dev/null +++ b/Tesses.CMS/Assets/AlbumsPage.html @@ -0,0 +1,24 @@ +
+ + + +
\ No newline at end of file diff --git a/Tesses.CMS/Assets/Devcenter.html b/Tesses.CMS/Assets/Devcenter.html index 65cccc3..cb4fd06 100644 --- a/Tesses.CMS/Assets/Devcenter.html +++ b/Tesses.CMS/Assets/Devcenter.html @@ -3,6 +3,5 @@ \ No newline at end of file diff --git a/Tesses.CMS/Assets/EditAlbumDetails.html b/Tesses.CMS/Assets/EditAlbumDetails.html new file mode 100644 index 0000000..8bfee6b --- /dev/null +++ b/Tesses.CMS/Assets/EditAlbumDetails.html @@ -0,0 +1,35 @@ +

Change album metadata

+
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+ + + +
+

Upload album art (uses JPEG)

+
+
+ + +
+ +
+ + +View/Edit extras +Tracklist \ No newline at end of file diff --git a/Tesses.CMS/Assets/EditEpisodeDetails.html b/Tesses.CMS/Assets/EditEpisodeDetails.html new file mode 100644 index 0000000..6f6d17f --- /dev/null +++ b/Tesses.CMS/Assets/EditEpisodeDetails.html @@ -0,0 +1,28 @@ +

Change movie metadata

+
+
+ + +
+
+ + +
+ + +
+

Upload episode files

+
+

You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)

+ +
+ + +
+ + +
diff --git a/Tesses.CMS/Assets/EditMovieDetails.html b/Tesses.CMS/Assets/EditMovieDetails.html index ff4614c..fbc6c2c 100644 --- a/Tesses.CMS/Assets/EditMovieDetails.html +++ b/Tesses.CMS/Assets/EditMovieDetails.html @@ -1,5 +1,5 @@

Change movie metadata

-
+
@@ -12,7 +12,7 @@

Upload movie files

-
+

You should do these in order (not required but recomended because you can only upload one at a time, that is in one tab at least)

- - + +
diff --git a/Tesses.CMS/Assets/EmailAlbum.html b/Tesses.CMS/Assets/EmailAlbum.html new file mode 100644 index 0000000..e7e38ff --- /dev/null +++ b/Tesses.CMS/Assets/EmailAlbum.html @@ -0,0 +1,10 @@ +

Hi {{propername}}

+

The album page for {{albumuserproper}}'s album {{albumproper}} has been {{if updated}} updated, {{else}} created, {{end}} you can view it here.

+ + +{{if hasmessage}} +
+

Message from {{albumuserproper}}

+

{{message}}

+
+{{end}} \ No newline at end of file diff --git a/Tesses.CMS/Assets/EmailMovie.html b/Tesses.CMS/Assets/EmailMovie.html new file mode 100644 index 0000000..d036dee --- /dev/null +++ b/Tesses.CMS/Assets/EmailMovie.html @@ -0,0 +1,9 @@ +

Hi {{propername}}

+

The movie page for {{movieuserproper}}'s movie {{movieproper}} has been {{if updated}} updated, {{else}} created, {{end}} you can view it here.

+ +{{if hasmessage}} +
+

Message from {{movieuserproper}}

+

{{message}}

+
+{{end}} \ No newline at end of file diff --git a/Tesses.CMS/Assets/EmailShow.html b/Tesses.CMS/Assets/EmailShow.html new file mode 100644 index 0000000..4e58dab --- /dev/null +++ b/Tesses.CMS/Assets/EmailShow.html @@ -0,0 +1,10 @@ +

Hi {{propername}}

+

The show page for {{showuserproper}}'s show {{showproper}} has been {{if updated}} updated, {{else}} created, {{end}} you can view it here.

+ + +{{if hasmessage}} +
+

Message from {{showuserproper}}

+

{{message}}

+
+{{end}} \ No newline at end of file diff --git a/Tesses.CMS/Assets/EpisodePage.html b/Tesses.CMS/Assets/EpisodePage.html new file mode 100644 index 0000000..667a759 --- /dev/null +++ b/Tesses.CMS/Assets/EpisodePage.html @@ -0,0 +1,38 @@ +
+
+ {{episodeproper}} +

{{seasonproper}}, {{episodeproper}}

+

{{showproper}}

+

{{userproper}}

+
+ {{if episodebrowserexists}} + Watch Online + {{end}} + {{if episodeexists}} + Download + {{end}} + {{if editable}} + Edit + Edit Subtitles + {{end}} + + + +
+
+
+

+ +

+
+
+
+

{{episodedescription}}

+
+
+
+
+ +
\ No newline at end of file diff --git a/Tesses.CMS/Assets/ExtrasViewer.html b/Tesses.CMS/Assets/ExtrasViewer.html index 7e2c66a..286187d 100644 --- a/Tesses.CMS/Assets/ExtrasViewer.html +++ b/Tesses.CMS/Assets/ExtrasViewer.html @@ -1,7 +1,7 @@

Extras path: {{path}}

{{if editable}}

Create Directory

- +
@@ -10,7 +10,7 @@

Upload File

-
+
diff --git a/Tesses.CMS/Assets/MailingList.html b/Tesses.CMS/Assets/MailingList.html index 13d80dd..86bcef1 100644 --- a/Tesses.CMS/Assets/MailingList.html +++ b/Tesses.CMS/Assets/MailingList.html @@ -1,4 +1,10 @@ +
+ + +
+ + +
+ \ No newline at end of file diff --git a/Tesses.CMS/Assets/ManageHtml.html b/Tesses.CMS/Assets/ManageHtml.html index 5eed12b..ac3477d 100644 --- a/Tesses.CMS/Assets/ManageHtml.html +++ b/Tesses.CMS/Assets/ManageHtml.html @@ -4,7 +4,7 @@
  • {{user.propername}} ({{user.name}}) -
    +
    diff --git a/Tesses.CMS/Assets/MoviePage.html b/Tesses.CMS/Assets/MoviePage.html index 0d493f3..0cc30dc 100644 --- a/Tesses.CMS/Assets/MoviePage.html +++ b/Tesses.CMS/Assets/MoviePage.html @@ -23,7 +23,15 @@ {{if torrentwextraexists}} Torrent With Extras {{end}} - + {{if editable}} + +
    + + +
    + + + {{end}}
    diff --git a/Tesses.CMS/Assets/MusicPlayerPage.html b/Tesses.CMS/Assets/MusicPlayerPage.html new file mode 100644 index 0000000..8aad4ee --- /dev/null +++ b/Tesses.CMS/Assets/MusicPlayerPage.html @@ -0,0 +1,289 @@ + +
    +
    +
      + +
    +
    +
    + +
    +

    +

    + + +

    + +
    + +
    +
    +
    + + + + 0:00 + + 0:00 + + Download + + +
    + diff --git a/Tesses.CMS/Assets/PageShell.html b/Tesses.CMS/Assets/PageShell.html index febdbc6..1881fe5 100644 --- a/Tesses.CMS/Assets/PageShell.html +++ b/Tesses.CMS/Assets/PageShell.html @@ -5,6 +5,10 @@ {{title}} + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Tesses.CMS/Assets/Webhook.html b/Tesses.CMS/Assets/Webhook.html new file mode 100644 index 0000000..7e7dd98 --- /dev/null +++ b/Tesses.CMS/Assets/Webhook.html @@ -0,0 +1,108 @@ +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    + + +
    +

    Type:

    +
    + + +
    +
    + + +
    +
    + + +
    +

    Enabled categories:

    +
    + + +
    +
    + + +
    +
    + + +
    + +
    +
    +
      + {{for webhook in webhooks}} +
    • +
      + + {{webhook.name}} ({{if webhook.ntfy}}Ntfy{{else if webhook.gotify}}Gotify{{else}}Other{{end}}) +
      + + +
      + +
      + + +
      + + + + {{if webhook.gotify || webhook.ntfy}} +
      + + +
      + {{end}} + +
      + + +
      +
      + + +
      +
      + + +
      + + + +
      +
    • + {{end}} +
    \ No newline at end of file diff --git a/Tesses.CMS/Assets/android-chrome-192x192.png b/Tesses.CMS/Assets/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..9e070f16806d9e9b7a7a0910cbbe38fea4c4eee1 GIT binary patch literal 11528 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

    p}d?fcsn_jTO*s6vpG-+um@F2n_!|g8gUCgaDtW zH|BMy(5JHtV);j$X8~mY+r*U^@-#B2S^pG`%dZT(`Ob~9uiX33GPU$3Mi;%}hsH;t zCvDV}9L~tEG{>s{04*Ap0-Pn}cTP#@u5vP#j=@zARPZ^tyK)g1yZxJ*GVL zsC$hdtIs^AgjS+0r|o-mu*^cEjubDv9@VW=XNsYo<<%ZQLltP@@_f4FFwQy4+hzp3 z0}_w4$9!$R8P+4EjmL!AxSWM3-wjr-QRQCGx6<%|jDO$dqX_N%th z?LNIbCxSYL0lIFj6^_mJH{DYYVSwbv{P~|a)6GUTSmGFf3fn1y$SG)OarxUcpVz~O zI53=W!lRxiO`+*v4V0bh+RM$Ixf&1geXV4OP60yNs+2}7C=sM%!Y5G&`Ke*Yo+__0 zKmX&|`Ns~h=VBA4N8Qv{^azZ|fQ;*sd;a`c<+*$)7FSU!L*{8e3Tl8iaw@V2Y2(2E E1L@Z8VgLXD literal 0 HcmV?d00001 diff --git a/Tesses.CMS/Assets/android-chrome-512x512.png b/Tesses.CMS/Assets/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..7885dd7c12fc9e3af390d9a262745235fdae5d0a GIT binary patch literal 52650 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 Ki#kP%u>S$MD-7TO literal 0 HcmV?d00001 diff --git a/Tesses.CMS/Assets/download.svg b/Tesses.CMS/Assets/download.svg new file mode 100644 index 0000000..6a171ea --- /dev/null +++ b/Tesses.CMS/Assets/download.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/favicon-16x16.png b/Tesses.CMS/Assets/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..d517f7b7f7bf651d94e8fd3a12a7955bf2c8683c GIT binary patch literal 617 zcmV-v0+#)WP)Px%BS}O-R5(vvQ#)?kKoEUHN{Ay%a$?I;3Ke^VRB0UO9=SmZ7XkwL6ajLI#PJzi z@B%3!yG!MQ0=5ZA*;bZ>mOIJpE+ru>2x2+=X5MEA@(=))mpmMf2;lG;e;@p30)c&H zI1SYu5YCA4n*kecwE$8sbrGB?^keo{+vZbZ`U3E0CaeHxfUHO1*rS~$pzWrMqeCSL z-5$M8j*=94{nX5@!HS7AFB8P$Z*;YGd9`-)ZS~(o z2Vfp2f~w<5lFb$;>g8#Qs&uHz#@^p-v?)2SAKiRw1c)_6qo`dgt)E~pNwbT^K=sc% zfw}|C+MzD3U4H+gtAahXt)UfxwTLZ#v}&vVIL&5PC#rXzz#A2P6rieXU`l6~-!{5@ zX!6G562LN2@UE)UsA9diEVaf(|;q3Beqw8|-2oW?( zMkGiFY48(Vd>X2>o1iKhdnXPQF0Y?>zBRl8L4)@Wh6>AK*& zdJ^m8B549!mi+qqkWLZGp9CX#nx?pZ@rpXz4bN>&D`w#+Dcr~HaLJ;4p1&Y;0KAF$ z2p-2}iYwGM+Apu{$Swy5+q#QJ+iuq*u1&jv|2q5yxjsZnjPk~s00000NkvXXu0mjf DpAi$3 literal 0 HcmV?d00001 diff --git a/Tesses.CMS/Assets/favicon-32x32.png b/Tesses.CMS/Assets/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..e585bd5985352e29f1e8ea4913cde18ee42c2438 GIT binary patch literal 1196 zcmV;d1XKHoP)Px(W=TXrR9Hu?R!efzN)$cUI0;Dxfg*tskQoURSO5bpA#Vrl#9Q%FsKS{m!}rKQ zHdPQR2Fu`299!z=zPHtCN!`jDJWp&(t-j};d(Kf4{-+NCP_@0>Yrgw>xxVgwEc8Hi zacz~iVqhn8xdI?u^uP2{K$Ep?4HWnEcL=}{fKx+JqJU}Jq`bviDJ*Y#M!==sb%2lh zM-KD>Trn5|RDIB<*N6L-ZtdgzhkP1f0MWKq55N=U001xnkif@CP!25`up_9iAw;A| z19-2Xbiq_F=>T!&0wRD+OATV|%*-NHd^coVOrf$xM4(=GAJO4)2i-%nCPTdZo%a_% z)Rhu4!;JVu;~}HeK=mdAIC}Xgdi~)sj$U_oyV;FOhnIKB&o66cggO+fT=kXFh}Y5j zVoN|cBCH@_0ME|4(c6myJU`oIuBXEdlEDg-(OPO*M)E6Dqhut|t*k4@5^{k9U>`5e zclojVMUMGx&ff(iqswJ7{<2AlwvZLYIdbco*3dFHn*aoPv~9U}a`q@Xzc|2)^F7O> zM2!BcFdYda>1f0ERYvjhch;Z($l10HjWm7OVnagj*&AmB5YKAyF$4nc=L(B)W*PZ& zlcyuCk?Tz=9v4Q)XhhTBG@RM2CX|Q>z2i4sW`H%obCb>s0Mew{QUU2GLo&E4HL{_2 zInDa>+W;etDAq~G^il*u@5C~&WASks>Y_FhWh6w38JQQ15HS1ZFhW3w5NCOrtOb0Z zVDMeJP(q466>iat3UUjafAG8D_-rS7d$Et_+J=f4 zgYLA7MRwq%HSV}yl;YImn%!h+a(S2hy3SHfR3T3NlB+UM#sSY^@ z5TL0a4OaMN0!wWWFaP9XPifQ!R!b5>lJGBaK~b=Nh|+wljU89ZocEvIEO#5Vc5mIh5k2Ls8*W!?o)Jl(m{8R;$b_Zco^jG8e6ViXxTY9y%MG&2 z(`n6?VaD7$od0D4h8_ybt$0GGpFLOAk7NOkJZbiDp_aZJ!Tt+}y^%AWftv&r zsAxbQP%#{E$^X(*J8>sA=&)ykd+7@f`dm-CYk1gh>~||!X1$*1WA^dWEF)qb;0DqKsDc8(*V7y*f)>F34qo7U_XYvAIoSXJ literal 0 HcmV?d00001 diff --git a/Tesses.CMS/Assets/pause.svg b/Tesses.CMS/Assets/pause.svg new file mode 100644 index 0000000..95bc792 --- /dev/null +++ b/Tesses.CMS/Assets/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/play.svg b/Tesses.CMS/Assets/play.svg new file mode 100644 index 0000000..52f0fcc --- /dev/null +++ b/Tesses.CMS/Assets/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/repeat_all.svg b/Tesses.CMS/Assets/repeat_all.svg new file mode 100644 index 0000000..35cc50b --- /dev/null +++ b/Tesses.CMS/Assets/repeat_all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/repeat_off.svg b/Tesses.CMS/Assets/repeat_off.svg new file mode 100644 index 0000000..c1b09d8 --- /dev/null +++ b/Tesses.CMS/Assets/repeat_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/repeat_one.svg b/Tesses.CMS/Assets/repeat_one.svg new file mode 100644 index 0000000..fa25413 --- /dev/null +++ b/Tesses.CMS/Assets/repeat_one.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/shuffle_off.svg b/Tesses.CMS/Assets/shuffle_off.svg new file mode 100644 index 0000000..4a31179 --- /dev/null +++ b/Tesses.CMS/Assets/shuffle_off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/shuffle_on.svg b/Tesses.CMS/Assets/shuffle_on.svg new file mode 100644 index 0000000..136131b --- /dev/null +++ b/Tesses.CMS/Assets/shuffle_on.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/site.webmanifest b/Tesses.CMS/Assets/site.webmanifest new file mode 100644 index 0000000..1050a0b --- /dev/null +++ b/Tesses.CMS/Assets/site.webmanifest @@ -0,0 +1 @@ +{"name":"{{title}}","short_name":"TessesCMS","icons":[{"src":"{{rooturl}}android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"{{rooturl}}android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#00ff00","background_color":"#ffffff","display":"standalone"} \ No newline at end of file diff --git a/Tesses.CMS/Assets/skip-next.svg b/Tesses.CMS/Assets/skip-next.svg new file mode 100644 index 0000000..ddc0c0d --- /dev/null +++ b/Tesses.CMS/Assets/skip-next.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/Assets/skip-prev.svg b/Tesses.CMS/Assets/skip-prev.svg new file mode 100644 index 0000000..71309a9 --- /dev/null +++ b/Tesses.CMS/Assets/skip-prev.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Tesses.CMS/CMSConfiguration.cs b/Tesses.CMS/CMSConfiguration.cs index b0f653a..d4a6c58 100644 --- a/Tesses.CMS/CMSConfiguration.cs +++ b/Tesses.CMS/CMSConfiguration.cs @@ -22,6 +22,8 @@ namespace Tesses.CMS public CMSPublish Publish {get;set;}=CMSPublish.NoRestriction; public List Urls { get; set; }=new List(); public string BrowserTranscode {get;set;}="-vf scale=640:480 -crf 28"; + + public string BrowserTranscodeMp3 {get;set;}="-b:a 160k"; public List BittorrentTrackers {get;set;}=new List(); public CMSNavUrl RelativeNavUrl(string text,string url) { diff --git a/Tesses.CMS/CSRF.cs b/Tesses.CMS/CSRF.cs new file mode 100644 index 0000000..ab0e3d1 --- /dev/null +++ b/Tesses.CMS/CSRF.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using Newtonsoft.Json; + +namespace Tesses.CMS +{ + public class CSRF + { + public long UserId {get;set;} + + public DateTime Expires {get;set;} + + public string Cookie {get;set;} + + public string CSRFToken {get;set;} + + public CSRF(long userId, string cookie) + { + UserId = userId; + Cookie = cookie; + Expires = DateTime.Now.AddMinutes(10); + byte[] data = new byte[32]; + using(var rng=RandomNumberGenerator.Create()) + rng.GetNonZeroBytes(data); + CSRFToken = Convert.ToBase64String(data); + } + } +} \ No newline at end of file diff --git a/Tesses.CMS/Class1.cs b/Tesses.CMS/Class1.cs index d349952..30af212 100644 --- a/Tesses.CMS/Class1.cs +++ b/Tesses.CMS/Class1.cs @@ -21,63 +21,63 @@ using System.Diagnostics; using System.Linq; using MailKit.Net.Smtp; using MimeKit; +using System.Net.Http; +using System.Runtime.Serialization; namespace Tesses.CMS { - public class Subtitle + public class Subtitle { [JsonProperty("end")] - public double End {get;set;} + public double End { get; set; } [JsonProperty("begin")] - public double Begin {get;set;} + public double Begin { get; set; } [JsonProperty("text")] - public string Text {get;set;} + public string Text { get; set; } public void ToWebVTT(TextWriter writer) { - var begin=TimeSpan.FromSeconds(Begin); + var begin = TimeSpan.FromSeconds(Begin); var end = TimeSpan.FromSeconds(End); - try{ + writer.WriteLine($"{begin.ToString("hh\\:mm\\:ss\\.fff")} --> {end.ToString("hh\\:mm\\:ss\\.fff")}"); - }catch(Exception ex) - { - Console.WriteLine(ex); - } + writer.WriteLine(HttpUtility.HtmlEncode(Text)); } public void ToSrt(TextWriter writer) { - var begin=TimeSpan.FromSeconds(Begin); + var begin = TimeSpan.FromSeconds(Begin); var end = TimeSpan.FromSeconds(End); writer.WriteLine($"{begin.ToString("hh\\:mm\\:ss\\,fff")} --> {end.ToString("hh\\:mm\\:ss\\,fff")}"); writer.WriteLine(Text); } - public static void ToWebVTT(TextWriter writer,List subtitles) + public static void ToWebVTT(TextWriter writer, List subtitles) { writer.WriteLine("WEBVTT"); - for(int i = 0;i subtitles) + public static void ToSrt(TextWriter writer, List subtitles) { - for(int i = 0;i0) writer.WriteLine(); - writer.WriteLine(i+1); + if (i > 0) writer.WriteLine(); + writer.WriteLine(i + 1); subtitles[i].ToSrt(writer); } } } public class CMSServer { - public static readonly Language[] Languages=new Language[]{ + public static readonly Language[] Languages = new Language[] + { new Language() { LangCode="en-US", @@ -236,76 +236,207 @@ namespace Tesses.CMS } }; - public class Language - { - public string LangCode {get;set;} - public string LangCodeVideo {get;set;} - public string LangTitle {get;set;} - } - - public MovieContentMetaData GetMovieContentMetaData(CMSConfiguration configuration, string user, string movie) - { - var _movie= new MovieContentMetaData(){ - PosterUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/poster.jpg", - ThumbnailUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/thumbnail.jpg", - BrowserStream = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/browser.mp4", - DownloadStream = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4", - MovieTorrentUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.torrent", - MovieWithExtrasTorrentUrl=$"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}_withextras.torrent" - }; - string subDir=Path.Combine(path,user, "movie",movie,"subtitles"); - if(Directory.Exists(subDir)) - foreach(var language in Directory.GetDirectories(subDir)) + public class Language { - string languageCode=Path.GetFileName(language); //en-US for english - _movie.SubtitlesStreams.Add(new SubtitleStream(){LanguageCode=languageCode, VttUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/subtitles/{languageCode}/{movie}.vtt", SrtUrl=$"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/subtitles/{languageCode}/{movie}.srt"}); + public string LangCode { get; set; } + public string LangCodeVideo { get; set; } + public string LangTitle { get; set; } } - string extrasPath = Path.Combine(path, user, "movie",movie,"extras"); - if(Directory.Exists(extrasPath)) - FindExtras( $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/extras/",extrasPath,_movie.ExtraStreams); - return _movie; - } - - private void FindExtras(string extras,string v, List extraStreams) - { - foreach(var d in Directory.GetDirectories(v)) + public ShowContentMetaData GetShowContentMetaData(string user, string show) { - var dirname = Path.GetFileName(d); - string url = $"{extras}{dirname}/"; + string showDir = Path.Combine(this.path, user, "show", show); + var _show = new ShowContentMetaData() + { + Info = provider.GetShow(user, show), + HasPoster = File.Exists(Path.Combine(path, user, "show", show, "poster.jpg")), + HasThumbnail = File.Exists(Path.Combine(path, user, "show", show, "thumbnail.jpg")), + HasShowTorrent = File.Exists(Path.Combine(path, user, "show", show, $"{show}.torrent")), + HasShowWithExtrasTorrent = File.Exists(Path.Combine(path, user, "show", show, $"{show}_withextras.torrent")), + PosterUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/poster.jpg", + ThumbnailUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg", + ShowTorrentUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/{show}.torrent", + ShowWithExtrasTorrentUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/{show}_withextras.torrent" + }; + string extrasPath = Path.Combine(path, user, "show", show, "extras"); + if (Directory.Exists(extrasPath)) + FindExtras($"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/extras/", extrasPath, _show.ExtraStreams); - ExtraDataStream dataStream=new ExtraDataStream(); - dataStream.IsDir=true; - dataStream.Name = dirname; - dataStream.Url = url; + return _show; + } + public SeasonContentMetaData GetSeasonContentMetaData(string user, string show, int season) + { + string showDir = Path.Combine(this.path, user, "show", show); + string episodeDir = Path.Combine(showDir, $"Season {season.ToString("D2")}"); + var _season0 = new SeasonContentMetaData() + { + Info = provider.GetSeason(user, show, season), + HasPoster = File.Exists(Path.Combine(episodeDir, "poster.jpg")) ? true : File.Exists(Path.Combine(this.path, user, "show", show, "poster.jpg")), + HasThumbnail = File.Exists(Path.Combine(episodeDir, "thumbnail.jpg")) ? true : File.Exists(Path.Combine(this.path, user, "show", show, "thumbnail.jpg")), + PosterUrl = File.Exists(Path.Combine(episodeDir, "poster.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/poster.jpg" : $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/poster.jpg", + ThumbnailUrl = File.Exists(Path.Combine(episodeDir, "thumbnail.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/thumbnail.jpg" : $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg", + }; - FindExtras(url,d,dataStream.Items); - extraStreams.Add(dataStream); + return _season0; + } + public EpisodeContentMetaData GetEpisodeContentMetaData(string user, string show, int season, int episode) + { + string episodeDir = Path.Combine(this.path, user, "show", show, $"Season {season.ToString("D2")}"); + var _episode = provider.GetEpisode(user, show, season, episode); + string name = $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}"; + var _episode0 = new EpisodeContentMetaData() + { + HasPoster = File.Exists(Path.Combine(episodeDir, $"{name}-poster.jpg")) ? true : (File.Exists(Path.Combine(episodeDir, "poster.jpg")) ? true : File.Exists(Path.Combine(this.path, user, "show", show, "poster.jpg"))), + HasThumbnail = File.Exists(Path.Combine(episodeDir, $"{name}-thumbnail.jpg")) ? true : (File.Exists(Path.Combine(episodeDir, "thumbnail.jpg")) ? true : File.Exists(Path.Combine(this.path, user, "show", show, "thumbnail.jpg"))), + HasBrowserStream = File.Exists(Path.Combine(episodeDir, $"S{season.ToString("D2")}E{episode.ToString("D2")}.mp4")), + HasDownloadStream = File.Exists(Path.Combine(episodeDir, $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}.mp4")), + PosterUrl = File.Exists(Path.Combine(episodeDir, $"{name}-poster.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/{_episode.EpisodeName}%20S{season.ToString("D2")}E{episode.ToString("D2")}-poster.jpg" : File.Exists(Path.Combine(episodeDir, "poster.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/poster.jpg" : $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/poster.jpg", + ThumbnailUrl = File.Exists(Path.Combine(episodeDir, $"{name}-thumbnail.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/{_episode.EpisodeName}%20S{season.ToString("D2")}E{episode.ToString("D2")}-thumbnail.jpg" : File.Exists(Path.Combine(episodeDir, "thumbnail.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/thumbnail.jpg" : $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg", + BrowserStream = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/S{season.ToString("D2")}E{episode.ToString("D2")}.mp4", + DownloadStream = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/{_episode.EpisodeName}%20S{season.ToString("D2")}E{episode.ToString("D2")}.mp4", + Info = provider.GetEpisode(user, show, season, episode) + }; + + GetEpisodeSubtitleStreams(_episode0.SubtitlesStreams, user, show, season, episode, _episode.EpisodeName); + + return _episode0; + } + public void GetEpisodeSubtitleStreams(List subtitleStreams, string user, string show, int season, int episode, string episode_name) + { + + string subDir = Path.Combine(path, user, "show", show, $"Season {season.ToString("D2")}", $"{episode_name} S{season.ToString("D2")}E{episode.ToString("D2")}-subtitles"); + + GetSubtitleStreams(subtitleStreams, subDir, $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/{episode_name}%20S{season.ToString("D2")}E{episode.ToString("D2")}-subtitles", $"{episode_name}%20S{season.ToString("D2")}E{episode.ToString("D2")}"); + } + public void GetMovieSubtitleStreams(List subtitleStreams, string user, string movie) + { + string subDir = Path.Combine(path, user, "movie", movie, "subtitles"); + GetSubtitleStreams(subtitleStreams, subDir, $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/subtitles", movie); + } + public void GetSubtitleStreams(List subtitleStreams, string subDir, string subUrl, string file) + { + + if (Directory.Exists(subDir)) + foreach (var language in Directory.GetDirectories(subDir)) + { + string languageCode = Path.GetFileName(language); //en-US for english + subtitleStreams.Add(new SubtitleStream() { LanguageCode = languageCode, VttUrl = $"{subUrl}/{languageCode}/{file}.vtt", SrtUrl = $"{subUrl}/{languageCode}/{file}.srt" }); + } + + } + public AlbumContentMetaData GetAlbumContentMetadata(string user, string album) + { + var _albumMeta = provider.GetAlbum(user, album); + List browserStreams = new List(); + List downloadStreams = new List(); + if (_albumMeta != null) + { + int trackNumber = 1; + foreach (var track in _albumMeta.Tracks) + { + string baseurl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}"; + string browserurl = $"{baseurl}/{HttpUtility.UrlPathEncode(track)}.mp3"; + string downloadurl = $"{baseurl}/{trackNumber.ToString("D2")}%20{HttpUtility.UrlPathEncode(_albumMeta.AlbumArtist)}%20-%20{HttpUtility.UrlPathEncode(track)}.flac"; + + if (File.Exists(Path.Combine(path, user, "album", album, $"{track}.mp3"))) + browserStreams.Add(new Track { Url = browserurl, Name = track, TrackNumber = trackNumber }); + + if (File.Exists(Path.Combine(path, user, "album", album, $"{trackNumber.ToString("D2")} {_albumMeta.AlbumArtist} - {track}.flac"))) + downloadStreams.Add(new Track { Url = downloadurl, Name = track, TrackNumber = trackNumber }); + + trackNumber++; + } + } + + var _album = new AlbumContentMetaData() + { + HasPoster = File.Exists(Path.Combine(path, user, "album", album, "poster.jpg")), + HasThumbnail = File.Exists(Path.Combine(path, user, "album", album, "thumbnail.jpg")), + HasAlbumTorrent = File.Exists(Path.Combine(path, user, "album", album, $"{album}.torrent")), + HasAlbumWithExtrasTorrent = File.Exists(Path.Combine(path, user, "album", album, $"{album}_withextras.torrent")), + PosterUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/poster.jpg", + ThumbnailUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/thumbnail.jpg", + AlbumTorrentUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/{album}.torrent", + AlbumWithExtrasTorrentUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/{album}_withextras.torrent", + DownloadStreams = downloadStreams, + BrowserStreams = browserStreams, + Info = _albumMeta + }; + string extrasPath = Path.Combine(path, user, "album", album, "extras"); + if (Directory.Exists(extrasPath)) + FindExtras($"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/extras/", extrasPath, _album.ExtraStreams); + return _album; } - foreach(var f in Directory.GetFiles(v)) + + public MovieContentMetaData GetMovieContentMetaData(string user, string movie) { - var filename = Path.GetFileName(f); - var url = $"{extras}{filename}"; - ExtraDataStream dataStream=new ExtraDataStream(); - dataStream.IsDir=false; - dataStream.Name = filename; - dataStream.Url = url; - - extraStreams.Add(dataStream); + var _movie = new MovieContentMetaData() + { + HasPoster = File.Exists(Path.Combine(path, user, "movie", movie, "poster.jpg")), + HasThumbnail = File.Exists(Path.Combine(path, user, "movie", movie, "thumbnail.jpg")), + HasBrowserStream = File.Exists(Path.Combine(path, user, "movie", movie, "browser.mp4")), + HasDownloadStream = File.Exists(Path.Combine(path, user, "movie", movie, $"{movie}.mp4")), + HasMovieTorrent = File.Exists(Path.Combine(path, user, "movie", movie, $"{movie}.torrent")), + HasMovieWithExtrasTorrent = File.Exists(Path.Combine(path, user, "movie", movie, $"{movie}_withextras.torrent")), + PosterUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/poster.jpg", + ThumbnailUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/thumbnail.jpg", + BrowserStream = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/browser.mp4", + DownloadStream = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4", + MovieTorrentUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.torrent", + MovieWithExtrasTorrentUrl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}_withextras.torrent" + }; + GetMovieSubtitleStreams(_movie.SubtitlesStreams, user, movie); + string extrasPath = Path.Combine(path, user, "movie", movie, "extras"); + if (Directory.Exists(extrasPath)) + FindExtras($"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/extras/", extrasPath, _movie.ExtraStreams); + return _movie; } - } - EmailCreator Creator {get;set;} + private void FindExtras(string extras, string v, List extraStreams) + { + foreach (var d in Directory.GetDirectories(v)) + { + var dirname = Path.GetFileName(d); + string url = $"{extras}{dirname}/"; + + ExtraDataStream dataStream = new ExtraDataStream(); + dataStream.IsDir = true; + dataStream.Name = dirname; + dataStream.Url = url; + + FindExtras(url, d, dataStream.Items); + extraStreams.Add(dataStream); + } + + foreach (var f in Directory.GetFiles(v)) + { + var filename = Path.GetFileName(f); + var url = $"{extras}{filename}"; + ExtraDataStream dataStream = new ExtraDataStream(); + dataStream.IsDir = false; + dataStream.Name = filename; + dataStream.Url = url; + + extraStreams.Add(dataStream); + } + } + + EmailCreator Creator { get; set; } Template pageShell; Template pageIndex; Template pageDevcenter; Template pageShow; Template pageSeason; + Template pageEpisode; Template pageShows; Template pageMovie; + Template pageAlbum; + Template pageMusicPlayer; + private Template pageAlbums; Template pageMovies; Template pageWatchMovie; + Template pageWatchEpisode; Template pageUpload; @@ -313,58 +444,80 @@ namespace Tesses.CMS Template pageEditMovieDetails; Template pageEditShowDetails; Template pageEditSeasonDetails; + Template pageEditEpisodeDetails; + Template pageEditAlbumDetails; Template pageExtrasViewer; Template pageEditUser; + private Template pageTracklist; Template aboutUser; Template pageSubtitleLangList; Template pageSubtitleEditor; - private Template pageVerifyEmail; - private Template pageManage; + + Template pageMailingList; + Template pageVerifyEmail; + Template pageManage; + + Template pageEmailMovie; + Template pageWebhook; + private Template siteWebMan; RouteServer routeServer; + + PathValueServer usersPathValueServer; PathValueServer moviePathValueServer; + PathValueServer albumPathValueServer; + PathValueServer showPathValueServer; PathValueServer seasonPathValueServer; + PathValueServer episodePathValueServer; + MountableServer showMountableServer; MountableServer usersMountableServer; MountableServer seasonMountableServer; + + RouteServer movieRouteServer; RouteServer showRouteServer; RouteServer seasonRouteServer; - + RouteServer episodeRouteServer; + private RouteServer albumRouteServer; IContentProvider provider; string path; - public CMSServer(string configpath,IContentProvider provider) + private event Action SendEvents; + public CMSServer(string configpath, IContentProvider provider) { - this.provider=provider; - usersMountableServer=new MountableServer(); + this.provider = provider; + usersMountableServer = new MountableServer(); usersPathValueServer = new PathValueServer(usersMountableServer); - movieRouteServer=new RouteServer(); + movieRouteServer = new RouteServer(); showRouteServer = new RouteServer(); - seasonRouteServer=new RouteServer(); + seasonRouteServer = new RouteServer(); + episodeRouteServer = new RouteServer(); + albumRouteServer = new RouteServer(); moviePathValueServer = new PathValueServer(movieRouteServer); - showMountableServer=new MountableServer(showRouteServer); - showPathValueServer=new PathValueServer(showMountableServer); + showMountableServer = new MountableServer(showRouteServer); + showPathValueServer = new PathValueServer(showMountableServer); + albumPathValueServer = new PathValueServer(albumRouteServer); seasonMountableServer = new MountableServer(seasonRouteServer); - seasonPathValueServer=new PathValueServer(seasonMountableServer); - - string configJson = Path.Combine(configpath,"config.json"); - if(File.Exists(configJson)) + seasonPathValueServer = new PathValueServer(seasonMountableServer); + episodePathValueServer = new PathValueServer(episodeRouteServer); + string configJson = Path.Combine(configpath, "config.json"); + if (File.Exists(configJson)) { Configuration = JsonConvert.DeserializeObject(File.ReadAllText(configJson)); } - if(Configuration.BittorrentTrackers.Count == 0) + if (Configuration.BittorrentTrackers.Count == 0) { - Configuration.BittorrentTrackers.AddRange( new string[]{ + Configuration.BittorrentTrackers.AddRange(new string[]{ "https://t1.hloli.org:443/announce", "http://1337.abcvg.info:80/announce", "http://tracker.renfei.net:8080/announce", @@ -373,21 +526,28 @@ namespace Tesses.CMS "https://tracker.foreverpirates.co:443/announce" }); } - Creator =new EmailCreator(Configuration); - pageShell=Template.Parse(AssetProvider.ReadAllText("/PageShell.html")); + Creator = new EmailCreator(Configuration); + pageShell = Template.Parse(AssetProvider.ReadAllText("/PageShell.html")); pageIndex = Template.Parse(AssetProvider.ReadAllText("/Index.html")); pageDevcenter = Template.Parse(AssetProvider.ReadAllText("/Devcenter.html")); pageMovie = Template.Parse(AssetProvider.ReadAllText("/MoviePage.html")); + pageAlbum = Template.Parse(AssetProvider.ReadAllText("/AlbumPage.html")); + pageMusicPlayer = Template.Parse(AssetProvider.ReadAllText("/MusicPlayerPage.html")); + pageAlbums = Template.Parse(AssetProvider.ReadAllText("/AlbumsPage.html")); pageMovies = Template.Parse(AssetProvider.ReadAllText("/MoviesPage.html")); pageUsers = Template.Parse(AssetProvider.ReadAllText("/UsersPage.html")); pageWatchMovie = Template.Parse(AssetProvider.ReadAllText("/WatchMovie.html")); + pageWatchEpisode = Template.Parse(AssetProvider.ReadAllText("/WatchEpisode.html")); pageUpload = Template.Parse(AssetProvider.ReadAllText("/Upload.html")); pageEditMovieDetails = Template.Parse(AssetProvider.ReadAllText("/EditMovieDetails.html")); pageEditShowDetails = Template.Parse(AssetProvider.ReadAllText("/EditShowDetails.html")); pageEditSeasonDetails = Template.Parse(AssetProvider.ReadAllText("/EditSeasonDetails.html")); + pageEditEpisodeDetails = Template.Parse(AssetProvider.ReadAllText("/EditEpisodeDetails.html")); + pageEditAlbumDetails = Template.Parse(AssetProvider.ReadAllText("/EditAlbumDetails.html")); pageExtrasViewer = Template.Parse(AssetProvider.ReadAllText("/ExtrasViewer.html")); pageEditUser = Template.Parse(AssetProvider.ReadAllText("/AccountEditor.html")); + pageTracklist = Template.Parse(AssetProvider.ReadAllText("/Tracklist.html")); aboutUser = Template.Parse(AssetProvider.ReadAllText("/AboutUser.html")); pageVerifyEmail = Template.Parse(AssetProvider.ReadAllText("/VerifyEmailWeb.html")); pageManage = Template.Parse(AssetProvider.ReadAllText("/ManageHtml.html")); @@ -395,51 +555,93 @@ namespace Tesses.CMS pageSubtitleEditor = Template.Parse(AssetProvider.ReadAllText("/SubtitleEditor.html")); pageShow = Template.Parse(AssetProvider.ReadAllText("/ShowPage.html")); pageSeason = Template.Parse(AssetProvider.ReadAllText("/SeasonPage.html")); + pageEpisode = Template.Parse(AssetProvider.ReadAllText("/EpisodePage.html")); pageShows = Template.Parse(AssetProvider.ReadAllText("/ShowsPage.html")); + pageMailingList = Template.Parse(AssetProvider.ReadAllText("/MailingList.html")); + pageEmailMovie = Template.Parse(AssetProvider.ReadAllText("/EmailMovie.html")); + pageWebhook = Template.Parse(AssetProvider.ReadAllText("/Webhook.html")); + siteWebMan = Template.Parse("/site.webmanifest"); MountableServer mountableServer = new MountableServer(new AssetProvider()); - path=Path.Combine(configpath,"content"); + path = Path.Combine(configpath, "content"); + + mountableServer.Mount("/content/", new StaticServer(path) { AllowListingDirectories = true }); + mountableServer.Mount("/api/v1/", CreateSwagme()); + mountableServer.Mount("/user/", usersPathValueServer); - mountableServer.Mount("/content/",new StaticServer(path){AllowListingDirectories=true}); - mountableServer.Mount("/api/v1/",CreateSwagme()); - mountableServer.Mount("/user/",usersPathValueServer); - routeServer = new RouteServer(mountableServer); - routeServer.Add("/",Index,"GET"); - routeServer.Add("/devcenter",Devcenter,"GET"); - routeServer.Add("/upload",UploadPage1,"GET"); - routeServer.Add("/upload",Upload,"POST"); - routeServer.Add("/users",UsersAsync,"GET"); - routeServer.Add("/login",LoginAsync); - routeServer.Add("/logout",LogoutAsync); - routeServer.Add("/login",LoginPostAsync,"POST"); - routeServer.Add("/signup",SignupAsync); - routeServer.Add("/signup",SignupPostAsync,"POST"); - routeServer.Add("/account",AccountAsync); - routeServer.Add("/account",AccountPostAsync,"POST"); - routeServer.Add("/verify",VerifyAsync); - routeServer.Add("/resend",ResendVerification,"GET"); - routeServer.Add("/manage",ManageAsync); - routeServer.Add("/manage",ManagePostAsync,"POST"); + routeServer.Add("/", Index, "GET"); + routeServer.Add("/site.webmanifest", SiteWebManifestAsync, "GET"); + routeServer.Add("/devcenter", Devcenter, "GET"); + routeServer.Add("/upload", UploadPage1, "GET"); + routeServer.Add("/upload", Upload, "POST"); + routeServer.Add("/users", UsersAsync, "GET"); + routeServer.Add("/login", LoginAsync); + routeServer.Add("/logout", LogoutAsync); + routeServer.Add("/login", LoginPostAsync, "POST"); + routeServer.Add("/signup", SignupAsync); + routeServer.Add("/signup", SignupPostAsync, "POST"); + routeServer.Add("/account", AccountAsync); + routeServer.Add("/account", AccountPostAsync, "POST"); + routeServer.Add("/verify", VerifyAsync); + routeServer.Add("/resend", ResendVerification, "GET"); + routeServer.Add("/manage", ManageAsync); + routeServer.Add("/manage", ManagePostAsync, "POST"); routeServer.Add("/impersonate", ImpersonateAsync); + RegisterUsersPath(); - Task.Factory.StartNew(async()=>{ - while(Running) + Task.Factory.StartNew(async () => + { + Stopwatch watch = new Stopwatch(); + watch.Start(); + while (Running) { - if(tasks.TryDequeue(out var item)) + if (tasks.TryDequeue(out var item)) { await item(); } await Task.Delay(TimeSpan.FromSeconds(0.125)); + if (watch.Elapsed.TotalMinutes > 10) + { + ClearExpiredCSRF(); + watch.Restart(); + } } }).Wait(0); } + + private void ClearExpiredCSRF() + { + lock (CsrfTokens) + { + List csrfs = new List(); + var dtNow = DateTime.Now; + foreach (var item in CsrfTokens) + { + if (item.Expires < dtNow) csrfs.Add(item); + } + foreach (var item in csrfs) + { + CsrfTokens.Remove(item); + } + } + } + + private async Task SiteWebManifestAsync(ServerContext ctx) + { + await ctx.SendTextAsync(await siteWebMan.RenderAsync(new + { + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title + }), "application/manifest+json"); + } + public async Task ManagePostAsync(ServerContext ctx) { ctx.ParseBody(); - var account=GetAccount(ctx); - if(account != null && account.IsAdmin) + var account = GetAccount(ctx, true); + if (account != null && account.IsAdmin) { - if(ctx.QueryParams.TryGetFirst("name",out var name)) + if (ctx.QueryParams.TryGetFirst("name", out var name)) { bool isinvited = ctx.QueryParams.ContainsKey("invited"); bool isverified = ctx.QueryParams.ContainsKey("verified"); @@ -455,15 +657,15 @@ namespace Tesses.CMS } public async Task ImpersonateAsync(ServerContext ctx) { - var account=GetAccount(ctx,out var cookie); - if(account != null && account.IsAdmin) + var account = GetAccount(ctx, out var cookie, true); + if (account != null && account.IsAdmin) { - if(ctx.QueryParams.TryGetFirst("user",out var user)) + if (ctx.QueryParams.TryGetFirst("user", out var user)) { var _user = provider.GetUserAccount(user); - if(_user != null) + if (_user != null) { - provider.ChangeSession(cookie,_user.Id); + provider.ChangeSession(cookie, _user.Id); } } } @@ -473,78 +675,87 @@ namespace Tesses.CMS private async Task ManageAsync(ServerContext ctx) { - var account=GetAccount(ctx); List users = new List(); - if(account != null && account.IsAdmin) + var account = GetAccount(ctx, out var cookie, false); List users = new List(); + if (account != null && account.IsAdmin) { - int i =0; - foreach(var user in provider.GetUsers()) + int i = 0; + foreach (var user in provider.GetUsers()) { - if(account.Id != user.Id) + if (account.Id != user.Id) { - string impersonate = $"{Configuration.Root.TrimEnd('/')}/impersonate?user={user.Username}"; - users.Add(new {nameattr=HttpUtility.HtmlAttributeEncode(user.Username), propername=HttpUtility.HtmlEncode(user.ProperName), name = HttpUtility.HtmlEncode(user.Username),isverified=user.IsVerified, isadmin = user.IsAdmin, isinvited=user.IsInvited,impersonate=impersonate,i=i}); - i++; + string csrf = ""; + + string csrf2 = ""; + if(account != null) + { + csrf=HttpUtility.UrlEncode(CreateCSRF(account.Id, cookie)); + csrf2=HttpUtility.HtmlAttributeEncode(CreateCSRF(account.Id, cookie)); + } + string impersonate = $"{Configuration.Root.TrimEnd('/')}/impersonate?user={user.Username}&csrf={csrf}"; + users.Add(new { csrf = csrf2, nameattr = HttpUtility.HtmlAttributeEncode(user.Username), propername = HttpUtility.HtmlEncode(user.ProperName), name = HttpUtility.HtmlEncode(user.Username), isverified = user.IsVerified, isadmin = user.IsAdmin, isinvited = user.IsInvited, impersonate = impersonate, i = i }); + i++; } } - + } - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageManage.RenderAsync(new{users=users}))); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageManage.RenderAsync(new { users = users }))); } private async Task ResendVerification(ServerContext ctx) { - var account=GetAccount(ctx); - if(account != null){ - if(account.IsVerified) + var account = GetAccount(ctx); + if (account != null) + { + if (account.IsVerified) await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/"); - else - { - var code=GetOrGenerateVerification(account); - await Creator.SendVerificationEmailAsync(account,code); - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageVerifyEmail.RenderAsync(new{Email=account.Email}))); - } - } + else + { + var code = GetOrGenerateVerification(account); + await Creator.SendVerificationEmailAsync(account, code); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageVerifyEmail.RenderAsync(new { Email = account.Email }))); + } + } } private async Task VerifyAsync(ServerContext ctx) { - bool failed=true; + bool failed = true; - if(ctx.QueryParams.TryGetFirst("token",out var token)) + if (ctx.QueryParams.TryGetFirst("token", out var token)) { var item = provider.GetVerificationAccount(token); - if(item.HasValue) + if (item.HasValue) { - var a=provider.GetUserById(item.Value); + var a = provider.GetUserById(item.Value); a.IsVerified = true; provider.UpdateUser(a); - failed=false; - + failed = false; + provider.DeleteVerificationCode(token); } - if(!failed) + if (!failed) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Your account has been verified, click login to login.

    Login")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Your account has been verified, click login to login.

    Login")); } } - if(failed) + if (failed) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Failed to verify account

    ")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed to verify account

    ")); } } private async Task AccountPostAsync(ServerContext ctx) { ctx.ParseBody(); - var account = GetAccount(ctx); - if(account != null) + var account = GetAccount(ctx,true); + if (account != null) { - if(ctx.QueryParams.TryGetFirst("about_me",out var about_me) && ctx.QueryParams.TryGetFirst("proper_name",out var proper_name)) + if (ctx.QueryParams.TryGetFirst("about_me", out var about_me) && ctx.QueryParams.TryGetFirst("proper_name", out var proper_name)) { account.AboutMe = about_me; account.ProperName = proper_name; provider.UpdateUser(account); - await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/"); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/"); } } else @@ -556,11 +767,11 @@ namespace Tesses.CMS private async Task AccountAsync(ServerContext ctx) { var account = GetAccount(ctx); - if(account != null) + if (account != null) { //account page - var res=new{admin = account.IsAdmin,Notverified = !account.IsVerified, Propername = HttpUtility.HtmlAttributeEncode(account.ProperName), Aboutme=HttpUtility.HtmlEncode(account.AboutMe)}; - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageEditUser.RenderAsync(res))); + var res = new { admin = account.IsAdmin, Notverified = !account.IsVerified, Propername = HttpUtility.HtmlAttributeEncode(account.ProperName), Aboutme = HttpUtility.HtmlEncode(account.AboutMe) }; + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditUser.RenderAsync(res))); } else { @@ -574,74 +785,74 @@ namespace Tesses.CMS await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/"); } - public bool Running =true; + public bool Running = true; public async Task LoginAsync(ServerContext ctx) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,await AssetProvider.ReadAllTextAsync("/LoginPage.html"))); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await AssetProvider.ReadAllTextAsync("/LoginPage.html"), false, false, true)); } public async Task SignupAsync(ServerContext ctx) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,await AssetProvider.ReadAllTextAsync("/SignupPage.html"))); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await AssetProvider.ReadAllTextAsync("/SignupPage.html"), false, false, true)); } public async Task SignupPostAsync(ServerContext ctx) { ctx.ParseBody(); - if(ctx.QueryParams.TryGetFirst("email", out var email) && ctx.QueryParams.TryGetFirst("name", out var name) && ctx.QueryParams.TryGetFirst("proper_name",out var proper_name) && ctx.QueryParams.TryGetFirst("password",out var password) && ctx.QueryParams.TryGetFirst("confirm_password",out var confirm_password)) + if (ctx.QueryParams.TryGetFirst("email", out var email) && ctx.QueryParams.TryGetFirst("name", out var name) && ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("password", out var password) && ctx.QueryParams.TryGetFirst("confirm_password", out var confirm_password)) { - bool error = false,emailTaken=false, nameTaken=false, properNameTaken=false, passwordDontMatch=false, passwordNotGoodEnough=false; - foreach(var user in provider.GetUsers()) + bool error = false, emailTaken = false, nameTaken = false, properNameTaken = false, passwordDontMatch = false, passwordNotGoodEnough = false; + foreach (var user in provider.GetUsers()) { - if(user.Username == name) + if (user.Username == name) { - nameTaken=true; - error=true; + nameTaken = true; + error = true; } - if(user.Email == email) - { - emailTaken=true; - error=true; - } - if(user.ProperName == proper_name) + if (user.Email == email) { - properNameTaken=true; - error=true; + emailTaken = true; + error = true; + } + if (user.ProperName == proper_name) + { + properNameTaken = true; + error = true; } } - if(password != confirm_password) + if (password != confirm_password) { - error=true; - passwordDontMatch=true; + error = true; + passwordDontMatch = true; } - if(password.Length < 10) + if (password.Length < 10) { - error=true; - passwordNotGoodEnough=true; + error = true; + passwordNotGoodEnough = true; } - if(error) + if (error) { - StringBuilder b=new StringBuilder(); - - if(emailTaken) + StringBuilder b = new StringBuilder(); + + if (emailTaken) { b.AppendLine("

    Email is taken

    "); } - if(nameTaken) + if (nameTaken) { b.AppendLine("

    Name is taken

    "); } - if(properNameTaken) + if (properNameTaken) { b.AppendLine("

    Proper Name is taken"); } - if(passwordNotGoodEnough) + if (passwordNotGoodEnough) { b.AppendLine("

    Password not good enough

    "); } - if(passwordDontMatch) + if (passwordDontMatch) { b.AppendLine("

    Passwords don't match

    "); } @@ -649,15 +860,15 @@ namespace Tesses.CMS } else { - provider.CreateUser(Configuration,name,proper_name,email,password); - var account=provider.GetUserAccount(name); - if(account.IsVerified) - await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/"); + provider.CreateUser(Configuration, name, proper_name, email, password); + var account = provider.GetUserAccount(name); + if (account.IsVerified) + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/"); else { - var code=GetOrGenerateVerification(account); - await Creator.SendVerificationEmailAsync(account,code); - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageVerifyEmail.RenderAsync(new{Email=email}))); + var code = GetOrGenerateVerification(account); + await Creator.SendVerificationEmailAsync(account, code); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageVerifyEmail.RenderAsync(new { Email = email }))); } } } @@ -665,64 +876,66 @@ namespace Tesses.CMS private string GetOrGenerateVerification(UserAccount account) { - var now=DateTime.Now; - - byte[] bytes=new byte[32]; - string token; + var now = DateTime.Now; - using(var rng = RandomNumberGenerator.Create()) - do { - - rng.GetBytes(bytes); - token=Convert.ToBase64String(bytes); - } while(provider.ContainsVerificationCode(token)); - provider.CreateVerificationCode(token,account.Id); - return token; + byte[] bytes = new byte[32]; + string token; + + using (var rng = RandomNumberGenerator.Create()) + do + { + + rng.GetBytes(bytes); + token = Convert.ToBase64String(bytes); + } while (provider.ContainsVerificationCode(token)); + provider.CreateVerificationCode(token, account.Id); + return token; } public async Task LoginPostAsync(ServerContext ctx) { ctx.ParseBody(); - if(ctx.QueryParams.TryGetFirst("email",out var email) && ctx.QueryParams.TryGetFirst("password",out var password)) + if (ctx.QueryParams.TryGetFirst("email", out var email) && ctx.QueryParams.TryGetFirst("password", out var password)) { - foreach(var a in provider.GetUsers()) + foreach (var a in provider.GetUsers()) { - if(a.Email != email) continue; - if(a.Email == email && a.PasswordCorrect(password)) + if (a.Email != email) continue; + if (a.Email == email && a.PasswordCorrect(password)) { //we got it - byte[] bytes=new byte[32]; + byte[] bytes = new byte[32]; string cookie; - using(var rng = RandomNumberGenerator.Create()) - do { - - rng.GetBytes(bytes); - cookie=Convert.ToBase64String(bytes); - } while(provider.ContainsSession(cookie)); + using (var rng = RandomNumberGenerator.Create()) + do + { - provider.CreateSession(cookie,a.Id); - ctx.ResponseHeaders.Add("Set-Cookie",$"Session={cookie}; Path=/"); + rng.GetBytes(bytes); + cookie = Convert.ToBase64String(bytes); + } while (provider.ContainsSession(cookie)); + + provider.CreateSession(cookie, a.Id); + ctx.ResponseHeaders.Add("Set-Cookie", $"Session={cookie}; Path=/"); await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/"); return; - } - - + } + + } - ctx.StatusCode=401; + ctx.StatusCode = 401; await ctx.SendTextAsync("

    Incorrect

    Home | Login"); - + } } private void Logout(ServerContext ctx) { - if(ctx.RequestHeaders.TryGetValue("Cookie",out var cookies)) - { - foreach(var c in cookies) + if (ctx.RequestHeaders.TryGetValue("Cookie", out var cookies)) + { + foreach (var c in cookies) { - var co = c.Split(new char[]{'='},2); - if(co.Length == 2 && co[0] == "Session") + var co = c.Split(new char[] { '=' }, 2); + if (co.Length == 2 && co[0] == "Session") { - if(provider.ContainsSession(co[1])) + if (provider.ContainsSession(co[1])) { provider.DeleteSession(co[1]); return; @@ -730,73 +943,304 @@ namespace Tesses.CMS } } } - + } - private UserAccount GetAccount(ServerContext ctx) + HttpClient client = new HttpClient(); + private async Task SendEvent(EventType type, string userpropername, string username, string name, string propername, string description, string body) { - return GetAccount(ctx,out var s); - } - private UserAccount GetAccount(ServerContext ctx,out string cookie) - { - cookie=""; - if(ctx.RequestHeaders.TryGetValue("Cookie",out var cookies)) - { - foreach(var c in cookies) + CMSEvent evt = new CMSEvent() { Type = type, Username = username, Name = name, ProperName = propername, UserProperName = userpropername, Description = description, Body = body }; + try + { + SendEvents?.Invoke(evt); + } + catch (Exception ex) { _ = ex; } + foreach (var user in provider.GetUsers()) + { + foreach (var wh in user.Webhooks) { - - var co = c.Split(new char[]{'='},2); - if(co.Length == 2 && co[0] == "Session") + if (wh.Username != username) continue; + if ((type == EventType.MovieCreate || type == EventType.MovieUpdate) && wh.EnabledMovies) + { + switch (wh.Type) + { + case WebHookType.Ntfy: + //ntfy is the easiest + { + + string con = $"Movie {propername} by {userpropername} was {(type == EventType.MovieCreate ? "created" : "updated")}"; + + try + { + await SendNtfyAsync($"{Configuration.Root.TrimEnd('/')}/user/{username}/movie/{name}/", wh.Url, wh.Key, wh.Priority, con, body); + //(await client.PostAsync(wh.Url,new StringContent(con))).Dispose(); + } + catch (Exception ex) + { + _ = ex; + } + } + break; + case WebHookType.Gotify: + { + string con = $"Movie {propername} by {userpropername} was {(type == EventType.MovieCreate ? "created" : "updated")}"; + + await SendGotifyAsync(wh.Url, wh.Key, wh.Priority, con, body); + } + break; + case WebHookType.Other: + (await client.PostAsync(wh.Url, new StringContent(JsonConvert.SerializeObject(new { key = wh.Key, data = evt }), Encoding.UTF8, "application/json"))).Dispose(); + break; + } + } + if ((type == EventType.ShowCreate || type == EventType.ShowUpdate) && wh.EnabledShows) + { + switch (wh.Type) + { + case WebHookType.Ntfy: + //ntfy is the easiest + { + + string con = $"Show {propername} by {userpropername} was {(type == EventType.ShowCreate ? "created" : "updated")}"; + + try + { + await SendNtfyAsync($"{Configuration.Root.TrimEnd('/')}/user/{username}/show/{name}/", wh.Url, wh.Key, wh.Priority, con, body); + + } + catch (Exception ex) + { + _ = ex; + } + } + break; + case WebHookType.Gotify: + { + string con = $"Show {propername} by {userpropername} was {(type == EventType.ShowCreate ? "created" : "updated")}"; + + await SendGotifyAsync(wh.Url, wh.Key, wh.Priority, con, body); + } + break; + case WebHookType.Other: + (await client.PostAsync(wh.Url, new StringContent(JsonConvert.SerializeObject(new { key = wh.Key, data = evt }), Encoding.UTF8, "application/json"))).Dispose(); + break; + } + } + if ((type == EventType.AlbumCreate || type == EventType.AlbumUpdate) && wh.EnabledShows) + { + switch (wh.Type) + { + case WebHookType.Ntfy: + //ntfy is the easiest + { + + string con = $"Album {propername} by {userpropername} was {(type == EventType.AlbumCreate ? "created" : "updated")}"; + + try + { + await SendNtfyAsync($"{Configuration.Root.TrimEnd('/')}/user/{username}/album/{name}/", wh.Url, wh.Key, wh.Priority, con, body); + + } + catch (Exception ex) + { + _ = ex; + } + } + break; + case WebHookType.Gotify: + { + string con = $"Album {propername} by {userpropername} was {(type == EventType.AlbumCreate ? "created" : "updated")}"; + + await SendGotifyAsync(wh.Url, wh.Key, wh.Priority, con, body); + } + break; + case WebHookType.Other: + (await client.PostAsync(wh.Url, new StringContent(JsonConvert.SerializeObject(new { key = wh.Key, data = evt }), Encoding.UTF8, "application/json"))).Dispose(); + break; + } + } + + } + } + } + + private async Task SendNtfyAsync(string cmsurl, string url, string key, int priority, string con, string body) + { + try + { + using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, url)) + { + request.Headers.Add("Title", con); + request.Headers.Add("Click", cmsurl); + request.Headers.Add("Priority", priority.ToString()); + if (!string.IsNullOrWhiteSpace(key)) + { + request.Headers.Add("Authorization", $"Bearer {key}"); + } + request.Content = new StringContent(body); + (await client.SendAsync(request)).Dispose(); + } + } + catch (Exception ex) + { + _ = ex; + } + } + + private async Task SendGotifyAsync(string url, string key, int prio, string con, string body) + { + try + { + HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Post, $"{url.TrimEnd('/')}/message"); + request.Headers.Add("X-Gotify-Key", key); + request.Content = new StringContent(JsonConvert.SerializeObject(new { message = body, title = con, priority = prio }), Encoding.UTF8, "application/json"); + (await client.SendAsync(request)).Dispose(); + + } + catch (Exception ex) + { + _ = ex; + } + } + private string CreateCSRF(long account, string cookie) + { + lock (CsrfTokens) + { + CSRF csrf = new CSRF(account, cookie); + CsrfTokens.Add(csrf); + return csrf.CSRFToken; + } + } + + + List CsrfTokens { get; set; } = new List(); + + private UserAccount GetAccount(ServerContext ctx, bool requiresCSRF = false) + { + + return GetAccount(ctx, out var s, requiresCSRF); + } + bool IsValidCSRFAndDestroy(long account, string cookie, string csrf) + { + lock (CsrfTokens) + { + + var now = DateTime.Now; + foreach (var token in CsrfTokens) + { + if (token.Expires > now && cookie == token.Cookie && csrf == token.CSRFToken && account == token.UserId) + { + CsrfTokens.Remove(token); + return true; + } + } + return false; + } + } + private UserAccount GetAccount(ServerContext ctx, out string cookie, bool requiresCSRF = false) + { + cookie = ""; + if (ctx.RequestHeaders.TryGetValue("Cookie", out var cookies)) + { + foreach (var c in cookies) + { + + var co = c.Split(new char[] { '=' }, 2); + if (co.Length == 2 && co[0] == "Session") { cookie = co[1]; long? account = provider.GetSession(cookie); - if(account.HasValue) - return provider.GetUserById(account.Value); + + + if (account.HasValue) + { + if (requiresCSRF) + { + if (ctx.QueryParams.TryGetFirst("csrf", out var csrf)) + { + if (IsValidCSRFAndDestroy(account.Value, cookie, csrf)) + { + return provider.GetUserById(account.Value); + } + } + throw new InvalidCSRFException(); + } + return provider.GetUserById(account.Value); + } } } } + else if (ctx.RequestHeaders.TryGetFirst("Authentication", out var auth)) + { + var co = auth.Split(new char[] { ' ' }, 2); + if (co.Length == 2 && co[0] == "Bearer") + { + long? account = provider.GetSession(co[1]); + if (account.HasValue) + return provider.GetUserById(account.Value); + } + } return null; } private async Task UsersAsync(ServerContext ctx) { - List users=new List(); - foreach(var user in provider.GetUsers()) + List users = new List(); + foreach (var user in provider.GetUsers()) { - if(!user.IsAdmin && Configuration.Publish == CMSPublish.Admin) - { - //await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You can't upload content

    ")); - continue; - } - if(!(user.IsAdmin || user.IsInvited) && Configuration.Publish == CMSPublish.RequireInvite) - { - //await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You can't upload content

    ")); - continue; - } - if(!(user.IsAdmin || user.IsInvited || user.IsVerified)) - { - //await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You can't upload content

    ")); - continue; - } + if (!user.IsAdmin && Configuration.Publish == CMSPublish.Admin) + { + //await ctx.SendTextAsync(await RenderHtmlAsync(ctx,"

    You can't upload content

    ")); + continue; + } + if (!(user.IsAdmin || user.IsInvited) && Configuration.Publish == CMSPublish.RequireInvite) + { + //await ctx.SendTextAsync(await RenderHtmlAsync(ctx,"

    You can't upload content

    ")); + continue; + } + if (!(user.IsAdmin || user.IsInvited || user.IsVerified)) + { + //await ctx.SendTextAsync(await RenderHtmlAsync(ctx,"

    You can't upload content

    ")); + continue; + } users.Add(user.Scriban()); } - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageUsers.RenderAsync(new { - Users=users, - rooturl=$"{Configuration.Root.TrimEnd('/')}/" + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageUsers.RenderAsync(new + { + Users = users, + rooturl = $"{Configuration.Root.TrimEnd('/')}/" }))); } - const string badC="/\\\"&,?|:;*@!# "; - private string FixString(string str) + const string badC = "/\\\"&,?|:;*@!# "; + const string badCKeepSpace = "/\\\"&,?|:;*@!#"; + private string FixStringKeepSpace(string str) { - StringBuilder b=new StringBuilder(); - foreach(var item in str) + StringBuilder b = new StringBuilder(); + foreach (var item in str) { - if(char.IsControl(item)) + if (char.IsControl(item)) { continue; } - - if(item >= 127) continue; - if(badC.Contains(item.ToString())) continue; + + if (item >= 127) continue; + if (badCKeepSpace.Contains(item.ToString())) continue; + b.Append(item.ToString()); + } + return b.ToString(); + + } + private string FixString(string str) + { + StringBuilder b = new StringBuilder(); + foreach (var item in str) + { + if (char.IsControl(item)) + { + continue; + } + + if (item >= 127) continue; + if (badC.Contains(item.ToString())) continue; b.Append(item.ToString()); } return b.ToString(); @@ -804,37 +1248,60 @@ namespace Tesses.CMS private async Task Upload(ServerContext ctx) { ctx.ParseBody(); - if(ctx.QueryParams.TryGetFirst("name",out var name) && ctx.QueryParams.TryGetFirst("proper_name",out var proper_name) && ctx.QueryParams.TryGetFirst("type", out var type) && ctx.QueryParams.TryGetFirst("description",out var description)) + if (ctx.QueryParams.TryGetFirst("name", out var name) && ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("type", out var type) && ctx.QueryParams.TryGetFirst("description", out var description)) { - var account=GetAccount(ctx); - if(account != null) + var account = GetAccount(ctx, true); + if (account != null) { - if(!account.IsAdmin && Configuration.Publish == CMSPublish.Admin) + if (!account.IsAdmin && Configuration.Publish == CMSPublish.Admin) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You can't upload content

    ")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    You can't upload content

    ")); return; } - if(!(account.IsAdmin || account.IsInvited) && Configuration.Publish == CMSPublish.RequireInvite) + if (!(account.IsAdmin || account.IsInvited) && Configuration.Publish == CMSPublish.RequireInvite) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You can't upload content

    ")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    You can't upload content

    ")); return; } - if(!(account.IsAdmin || account.IsInvited || account.IsVerified)) + if (!(account.IsAdmin || account.IsInvited || account.IsVerified)) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You can't upload content

    ")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    You can't upload content

    ")); return; } name = FixString(name); - switch(type) + switch (type) { case "movie": - provider.CreateMovie(account.Username,name,proper_name,description.Replace("\r","")); + var movie = provider.CreateMovie(account.Username, name, proper_name, description.Replace("\r", "")); + ScheduleTask(async () => + { + await this.Creator.EmailMovieAsync(provider, account, movie, false); + + await SendEvent(EventType.MovieCreate, account.ProperName, account.Username, movie.Name, movie.ProperName, movie.Description, ""); + + }); await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{account.Username}/movie/{name}/edit"); break; case "show": - provider.CreateShow(account.Username,name,proper_name,description); + var show = provider.CreateShow(account.Username, name, proper_name, description); + ScheduleTask(async () => + { + await this.Creator.EmailShowAsync(provider, account, show, false); + await SendEvent(EventType.ShowCreate, account.ProperName, account.Username, show.Name, show.ProperName, show.Description, ""); + + }); await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{account.Username}/show/{name}/edit"); break; + case "album": + var album = provider.CreateAlbum(account.Username, name, proper_name, description); + ScheduleTask(async () => + { + await this.Creator.EmailAlbumAsync(provider, account, album, false); + await SendEvent(EventType.AlbumCreate, account.ProperName, account.Username, album.Name, album.ProperName, album.Description, ""); + + }); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{account.Username}/album/{name}/edit"); + break; } } @@ -852,319 +1319,1157 @@ namespace Tesses.CMS public async Task UploadPage1(ServerContext ctx) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,await RenderUpload1Async(),Configuration.RelativeNavUrl("Devcenter","devcenter"))); - + var account = GetAccount(ctx, out var cookie); + if (account != null) + { + var csrf = HttpUtility.HtmlAttributeEncode(CreateCSRF(account.Id, cookie)); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await RenderUpload1Async(csrf), false, false, false, true)); + } } public IServer Movies() { - RouteServer routeServer=new RouteServer(); - routeServer.Add("/",MoviesAsync); + RouteServer routeServer = new RouteServer(); + routeServer.Add("/", MoviesAsync); return routeServer; } public async Task MoviesAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - List movies=new List(); - foreach(var item in provider.GetMovies(user)) + string user = usersPathValueServer.GetValue(ctx); + List movies = new List(); + foreach (var item in provider.GetMovies(user)) { - var data = GetMovieContentMetaData(Configuration,user,item.Name); + var data = GetMovieContentMetaData(user, item.Name); movies.Add(item.Scriban(data.ThumbnailUrl)); } - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageMovies.RenderAsync(new{Movies=movies}))); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageMovies.RenderAsync(new { Movies = movies }))); } private void RegisterUsersPath() { - RouteServer routeServer=new RouteServer(); - routeServer.Add("/",UserPageAsync); - routeServer.Add("/about",AboutAsync); - usersMountableServer.Mount("/",routeServer); - usersMountableServer.Mount("/movies/",Movies()); - usersMountableServer.Mount("/movie/",moviePathValueServer); - usersMountableServer.Mount("/shows/",Shows()); - usersMountableServer.Mount("/show/",showPathValueServer); + RouteServer routeServer = new RouteServer(); + routeServer.Add("/", UserPageAsync); + routeServer.Add("/about", AboutAsync); + routeServer.Add("/mailinglist", MailingListAsync); + routeServer.Add("/mailinglist", MailingListPostAsync, "POST"); + routeServer.Add("/webhooks", Webhooks); + routeServer.Add("/update_webhook", UpdateWebhook, "POST"); + routeServer.Add("/create_webhook", CreateWebhook, "POST"); + usersMountableServer.Mount("/", routeServer); + usersMountableServer.Mount("/movies/", Movies()); + usersMountableServer.Mount("/movie/", moviePathValueServer); + usersMountableServer.Mount("/shows/", Shows()); + usersMountableServer.Mount("/show/", showPathValueServer); + usersMountableServer.Mount("/albums/", Albums()); + usersMountableServer.Mount("/album/", albumPathValueServer); RegisterMoviePath(); RegisterShowPath(); + RegisterAlbumPath(); } + private async Task CreateWebhook(ServerContext ctx) + { + ctx.ParseBody(); + string username = usersPathValueServer.GetValue(ctx); + var theAccount = provider.GetUserAccount(username); + var account = GetAccount(ctx,true); + if (account != null) + { + if (!ctx.QueryParams.TryGetFirst("key", out var key)) key = ""; + if (!ctx.QueryParams.TryGetFirstInt32("priority", out var priority)) priority = 2; + bool enablemovies = ctx.QueryParams.GetFirstBoolean("enablemovies"); + bool enableshows = ctx.QueryParams.GetFirstBoolean("enableshows"); + bool enablealbums = ctx.QueryParams.GetFirstBoolean("enablealbums"); + bool enablesoftware = ctx.QueryParams.GetFirstBoolean("enablesoftware"); + bool enableother = ctx.QueryParams.GetFirstBoolean("enableother"); + if (ctx.QueryParams.TryGetFirst("name", out var name) && ctx.QueryParams.TryGetFirst("url", out var url) && ctx.QueryParams.TryGetFirst("type", out var type)) + { + WebHook hook = new WebHook + { + Username = username, + Priority = priority, + Url = url, + WebhookName = name, + Type = type == "ntfy" ? WebHookType.Ntfy : (type == "gotify" ? WebHookType.Gotify : WebHookType.Other), + Key = key, + EnabledMovies = enablemovies, + EnabledShows = enableshows, + EnabledAlbums = enablealbums, + EnabledSoftware = enablesoftware, + EnabledOther = enableother + }; + account.Webhooks.Add(hook); + provider.UpdateUser(account); + } + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{username}/webhooks"); + } + else + { + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/login"); + } + } + + private async Task UpdateWebhook(ServerContext ctx) + { + ctx.ParseBody(); + string username = usersPathValueServer.GetValue(ctx); + var theAccount = provider.GetUserAccount(username); + var account = GetAccount(ctx,true); + if (account != null) + { + if (!ctx.QueryParams.TryGetFirst("key", out var key)) key = ""; + if (!ctx.QueryParams.TryGetFirstInt32("priority", out var priority)) priority = 2; + + bool enablemovies = ctx.QueryParams.GetFirstBoolean("enablemovies"); + bool enableshows = ctx.QueryParams.GetFirstBoolean("enableshows"); + bool enablealbums = ctx.QueryParams.GetFirstBoolean("enablealbums"); + bool enablesoftware = ctx.QueryParams.GetFirstBoolean("enablesoftware"); + bool enableother = ctx.QueryParams.GetFirstBoolean("enableother"); + if (ctx.QueryParams.TryGetFirst("name", out var name) && ctx.QueryParams.TryGetFirst("url", out var url)) + { + foreach (var item in account.Webhooks) + { + if (item.WebhookName == name && item.Username == username) + { + if (ctx.QueryParams.TryGetFirst("action", out var val) && val == "Delete") + { + account.Webhooks.Remove(item); + provider.UpdateUser(account); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{username}/webhooks"); + return; + } + item.EnabledMovies = enablemovies; + item.EnabledShows = enableshows; + item.EnabledAlbums = enablealbums; + item.EnabledSoftware = enablesoftware; + item.EnabledOther = enableother; + item.Key = key; + item.Priority = priority; + item.Url = url; + + provider.UpdateUser(account); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{username}/webhooks"); + return; + } + } + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{username}/webhooks"); + } + else + { + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/login"); + } + } + } + + private async Task Webhooks(ServerContext ctx) + { + string username = usersPathValueServer.GetValue(ctx); + var theAccount = provider.GetUserAccount(username); + var account = GetAccount(ctx); + if (account != null) + { + List webhooks = new List(); + foreach (var item in account.Webhooks) + { + if (item.Username != username) continue; + + webhooks.Add(new { name = item.WebhookName, ntfy = item.Type == WebHookType.Ntfy, gotify = item.Type == WebHookType.Gotify, priority = item.Priority, url = item.Url, key = item.Key, enablemovies = item.EnabledMovies, enableshows = item.EnabledShows, enablealbums = item.EnabledAlbums, enablesoftware = item.EnabledSoftware, enableother = item.EnabledOther }); + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageWebhook.RenderAsync(new + { + webhooks + }))); + } + else + { + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/login"); + } + } + + private async Task MailingListPostAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + var theAccount = provider.GetUserAccount(user); + var account = GetAccount(ctx,true); + if (account != null) + { + ctx.ParseBody(); + MailEntry entry = null; + foreach (var item in theAccount.AccountsToMail) + { + if (item.UserId == account.Id) + { + entry = item; + break; + } + } + if (entry == null) + { + entry = new MailEntry() { UserId = account.Id }; + theAccount.AccountsToMail.Add(entry); + } + entry.EnableMovies = ctx.QueryParams.ContainsKey("enablemovies"); + entry.EnableShows = ctx.QueryParams.ContainsKey("enableshows"); + entry.EnableSingles = ctx.QueryParams.ContainsKey("enablesingles"); + entry.EnableAlbums = ctx.QueryParams.ContainsKey("enablealbums"); + entry.EnableSoftware = ctx.QueryParams.ContainsKey("enablesoftware"); + entry.EnableOther = ctx.QueryParams.ContainsKey("enableother"); + entry.EnableUpdates = ctx.QueryParams.ContainsKey("enableupdates"); + + provider.UpdateUser(theAccount); + } + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/"); + } + private async Task MailingListAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + var theAccount = provider.GetUserAccount(user); + var account = GetAccount(ctx); + if (account != null) + { + MailEntry entry = new MailEntry() { UserId = account.Id }; + foreach (var item in theAccount.AccountsToMail) + { + if (item.UserId == account.Id) + { + entry = item; + break; + } + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageMailingList.RenderAsync(new + { + enablemovies = entry.EnableMovies, + enableshows = entry.EnableShows, + enablesingles = entry.EnableSingles, + enablealbums = entry.EnableAlbums, + enablesoftware = entry.EnableSoftware, + enableother = entry.EnableOther, + enableupdates = entry.EnableUpdates + }))); + return; + } + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/login"); + } + private async Task EditAlbumPagePostAsync(ServerContext ctx) + { + ctx.ParseBody(); + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _album = provider.GetAlbum(user, album); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + if (_album != null) + { + if (ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("description", out var description) && ctx.QueryParams.TryGetFirst("album_artist", out var album_artst) && ctx.QueryParams.TryGetFirst("year", out var yearS) && int.TryParse(yearS, out var year)) + { + _album.ProperName = proper_name; + _album.Description = description.Replace("\r", ""); + _album.AlbumArtist = FixStringKeepSpace(album_artst); + _album.Year = year; + provider.UpdateAlbum(_album); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); + return; + } + } + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + + } + private void RegisterAlbumPath() + { + albumRouteServer.Add("/", AlbumPageAsync); + albumRouteServer.Add("/play", PlayAlbumAsync); + albumRouteServer.Add("/edit", EditAlbumPageAsync); + albumRouteServer.Add("/edit", EditAlbumPagePostAsync, "POST"); + albumRouteServer.Add("/upload", UploadAlbumStreamAsync, "POST"); + albumRouteServer.Add("/sendupdate", SendAlbumUpdateAsync, "POST"); + albumRouteServer.Add("/edit_tracklist", EditTracklistAsync); + albumRouteServer.Add("/edit_tracklist", EditTracklistPostAsync, "POST"); + albumRouteServer.Add("/upload_extra", UploadAlbumExtraAsync, "POST"); + albumRouteServer.Add("/extras", ExtrasAlbumPageAsync); + albumRouteServer.Add("/mkdir", ExtrasAlbumMkdirAsync, "POST"); + + } + private async Task EditTracklistPostAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + + var _album = provider.GetAlbum(user, album); + var _user = provider.GetUserAccount(user); + + var me = GetAccount(ctx,true); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (_album != null && me != null && _user != null) + { + Directory.CreateDirectory(Path.Combine(path, user, "album", album)); + var tmpFile = Path.Combine(path, user, "album", album, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + foreach (var item in ctx.ParseBody((n, fn, ct) => File.Create(tmpFile))) + item.Value.Dispose(); + + if (!ctx.QueryParams.TryGetFirst("operation", out var operation)) + operation = ""; + + + if (!ctx.QueryParams.TryGetFirstInt32("track_id", out var track_id)) + track_id = 0; + + switch (operation) + { + case "Yes": + { + string oldtrack = _album.Tracks[track_id]; + string flac = Path.Combine(path, user, "album", album, $"{(track_id + 1).ToString("D2")} {_album.AlbumArtist} - {oldtrack}.flac"); + if (File.Exists(flac)) File.Delete(flac); + + string oldmp3 = Path.Combine(path, user, "album", album, $"{oldtrack}.mp3"); + if (File.Exists(oldmp3)) File.Delete(oldmp3); + + + + _album.Tracks.RemoveAt(track_id); + for (int i = track_id; i < _album.Tracks.Count; i++) + { + string oldflac = Path.Combine(path, user, "album", album, $"{(track_id + 2).ToString("D2")} {_album.AlbumArtist} - {oldtrack}.flac"); + string newflac = Path.Combine(path, user, "album", album, $"{(track_id + 1).ToString("D2")} {_album.AlbumArtist} - {oldtrack}.flac"); + + if (File.Exists(oldflac)) File.Move(oldflac, newflac); + } + provider.UpdateAlbum(_album); + } + break; + case "Delete": + { + string oldtrack = _album.Tracks[track_id]; + string text = $"

    Delete Track: {oldtrack}?

    Note the media files will be removed
    No
    "; + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, text)); + return; + } + + case "Upload": + { + string oldtrack = _album.Tracks[track_id]; + string flac = Path.Combine(path, user, "album", album, $"{(track_id + 1).ToString("D2")} {_album.AlbumArtist} - {oldtrack}.flac"); + + File.Move(tmpFile, flac); + + ScheduleTask(async () => + { + await GenerateBittorentFileAlbumAsync(user, album); + }); + + string oldmp3 = Path.Combine(path, user, "album", album, $"{oldtrack}.mp3"); + + ScheduleFFmpeg($"-y -i \"{flac}\" {Configuration.BrowserTranscodeMp3} \"{oldmp3}\""); + } + break; + case "Rename": + { + if (track_id < _album.Tracks.Count) + { + if (!ctx.QueryParams.TryGetFirst("track_name", out var track_name)) + track_name = $"Track {track_id.ToString("D2")}"; + + track_name = FixStringKeepSpace(track_name); + + string oldtrack = _album.Tracks[track_id]; + string oldflac = Path.Combine(path, user, "album", album, $"{(track_id + 1).ToString("D2")} {_album.AlbumArtist} - {oldtrack}.flac"); + string oldmp3 = Path.Combine(path, user, "album", album, $"{oldtrack}.mp3"); + string newflac = Path.Combine(path, user, "album", album, $"{(track_id + 1).ToString("D2")} {_album.AlbumArtist} - {track_name}.flac"); + string newmp3 = Path.Combine(path, user, "album", album, $"{track_name}.mp3"); + + if (oldflac != newflac && File.Exists(oldflac)) + File.Move(oldflac, newflac); + if (oldmp3 != newmp3 && File.Exists(oldmp3)) + File.Move(oldmp3, newmp3); + + _album.Tracks[track_id] = track_name; + provider.UpdateAlbum(_album); + } + } + break; + case "Add": + { + if (!ctx.QueryParams.TryGetFirst("track_name", out var track_name)) + track_name = $"Track {track_id.ToString("D2")}"; + + _album.Tracks.Add(FixStringKeepSpace(track_name)); + provider.UpdateAlbum(_album); + } + break; + } + + if (File.Exists(tmpFile)) + File.Delete(tmpFile); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/album/{album}/edit_tracklist"); + } + } + private async Task EditTracklistAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + + var _album = provider.GetAlbum(user, album); + var _user = provider.GetUserAccount(user); + + var me = GetAccount(ctx,out var cookie); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (_album != null && me != null && _user != null) + { + List tracks = new List(); + string csrf=""; + if(me != null) + csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + + for (int i = 0; i < _album.Tracks.Count; i++) + { + tracks.Add(new { index = i, name = HttpUtility.HtmlAttributeEncode(_album.Tracks[i]) }); + } + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageTracklist.RenderAsync(new { tracks,csrf }))); + } + + } + + private async Task PlayAlbumAsync(ServerContext ctx) + { + + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + + var _album = provider.GetAlbum(user, album); + var _user = provider.GetUserAccount(user); + + var me = GetAccount(ctx); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (_album != null && _user != null) + { + List list = new List(); + int i = 1; + foreach (var item in _album.Tracks) + { + list.Add(new + { + artist = _album.AlbumArtist, + album = _album.ProperName, + name = item, + art = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/thumbnail.jpg", + url = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/{HttpUtility.UrlPathEncode(item)}.mp3", + download = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/{i.ToString("D2")}%20{HttpUtility.UrlPathEncode(_album.AlbumArtist)}%20-%20{HttpUtility.UrlPathEncode(item)}.flac" + }); + i++; + } + + object v = new + { + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + list = JsonConvert.SerializeObject(list) + }; + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageMusicPlayer.RenderAsync(v))); + } + } + private async Task AlbumPageAsync(ServerContext ctx) + { + + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + + var _album = provider.GetAlbum(user, album); + var _user = provider.GetUserAccount(user); + + var me = GetAccount(ctx,out var cookie); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + object value; + if (_album != null && _user != null) + { + string albumDir = Path.Combine(this.path, user, "album", album); + bool torrent = File.Exists(Path.Combine(albumDir, $"{album}.torrent")); + bool torrent_wextra = File.Exists(Path.Combine(albumDir, $"{album}_withextras.torrent")); + bool extrasexists = Directory.Exists(Path.Combine(albumDir, "extras")) || me != null; + + string thumb = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/thumbnail.jpg"; + string csrf=""; + if(me != null) + csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + value = new + { + csrf, + extrasexists, + torrentexists = torrent, + torrentwextraexists = torrent_wextra, + torrent = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/{album}.torrent", + torrentwextra = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/{album}_withextras.torrent", + editable = me != null, + userproper = HttpUtility.HtmlEncode(_user.ProperName), + username = HttpUtility.HtmlEncode(user), + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasalbum = true, + albumthumbnail = thumb, + albumproper = HttpUtility.HtmlEncode(_album.ProperName), + albumname = HttpUtility.HtmlEncode(_album.Name), + albumdescription = DescriptLinkUtils(_album.Description ?? "").Replace("\n", "
    ") + }; + } + else + { + value = new + { + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasalbum = false + }; + } + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageAlbum.RenderAsync(value))); + + + + } private void RegisterShowPath() { - showRouteServer.Add("/",ShowPageAsync); - showRouteServer.Add("/edit",EditShowPageAsync); - showRouteServer.Add("/edit",EditShowPagePostAsync,"POST"); - showRouteServer.Add("/upload",UploadShowStreamAsync,"POST"); - showRouteServer.Add("/addseason",AddSeasonAsync,"POST"); - showMountableServer.Mount("/season/",seasonPathValueServer); + showRouteServer.Add("/", ShowPageAsync); + showRouteServer.Add("/edit", EditShowPageAsync); + showRouteServer.Add("/edit", EditShowPagePostAsync, "POST"); + showRouteServer.Add("/upload", UploadShowStreamAsync, "POST"); + showRouteServer.Add("/addseason", AddSeasonAsync, "POST"); + showRouteServer.Add("/upload_extra", UploadShowExtraAsync, "POST"); + showRouteServer.Add("/extras", ExtrasShowPageAsync); + showRouteServer.Add("/mkdir", ExtrasShowMkdirAsync, "POST"); + showRouteServer.Add("/sendupdate", SendShowUpdateAsync, "POST"); + + showMountableServer.Mount("/season/", seasonPathValueServer); RegisterSeasonPath(); } private void RegisterSeasonPath() { - seasonRouteServer.Add("/",SeasonPageAsync); - seasonRouteServer.Add("/edit",EditSeasonPageAsync); - seasonRouteServer.Add("/edit",EditSeasonPagePostAsync,"POST"); - seasonRouteServer.Add("/addepisode",AddEpisodeAsync,"POST"); - seasonRouteServer.Add("/upload",UploadSeasonStreamAsync,"POST"); + seasonRouteServer.Add("/", SeasonPageAsync); + seasonRouteServer.Add("/edit", EditSeasonPageAsync); + seasonRouteServer.Add("/edit", EditSeasonPagePostAsync, "POST"); + seasonRouteServer.Add("/addepisode", AddEpisodeAsync, "POST"); + seasonRouteServer.Add("/upload", UploadSeasonStreamAsync, "POST"); + + seasonMountableServer.Mount("/episode/", episodePathValueServer); + RegisterEpisodePath(); + } + + private void RegisterEpisodePath() + { + episodeRouteServer.Add("/", EpisodePageAsync); + episodeRouteServer.Add("/edit", EditEpisodePageAsync); + episodeRouteServer.Add("/edit", EditEpisodePagePostAsync, "POST"); + episodeRouteServer.Add("/upload", UploadEpisodeStreamAsync, "POST"); + episodeRouteServer.Add("/subtitles", SubtitlesEpisodeAsync); + episodeRouteServer.Add("/subtitles", SubtitlesEpisodePostAsync, "POST"); + episodeRouteServer.Add("/play", PlayEpisodePageAsync); + } + + private async Task EpisodePageAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + + var _show = provider.GetShow(user, show); + + var _user = provider.GetUserAccount(user); + + var me = GetAccount(ctx); + + string seasonS = seasonPathValueServer.GetValue(ctx); + string episodeS = episodePathValueServer.GetValue(ctx); + + if (!int.TryParse(seasonS, out var season)) season = 1; + if (!int.TryParse(episodeS, out var episode)) episode = 1; + + var _season = provider.GetSeason(user, show, season); + var _episode = provider.GetEpisode(user, show, season, episode); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + object value; + if (_show != null && _user != null) + { + string episodeDir = Path.Combine(this.path, user, "show", show, $"Season {season.ToString("D2")}"); + bool episodebrowserexists = File.Exists(Path.Combine(episodeDir, $"S{season.ToString("D2")}E{episode.ToString("D2")}.mp4")); + string name = $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}"; + bool episodeexists = File.Exists(Path.Combine(episodeDir, $"{name}.mp4")); + string thumb = File.Exists(Path.Combine(episodeDir, $"{name}-thumbnail.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/{_episode.EpisodeName}%20S{season.ToString("D2")}E{episode.ToString("D2")}-thumbnail.jpg" : File.Exists(Path.Combine(episodeDir, "thumbnail.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/thumbnail.jpg" : $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"; + value = new + { + episodebrowserexists, + episodeexists, + downloadurl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/{_episode.EpisodeName}%20S{season.ToString("D2")}E{episode.ToString("D2")}.mp4", + editable = me != null, + userproper = HttpUtility.HtmlEncode(_user.ProperName), + username = HttpUtility.HtmlEncode(user), + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasmovie = true, + episodethumbnail = thumb, + seasonproper = HttpUtility.HtmlEncode(_season.ProperName), + showpropername = HttpUtility.HtmlEncode(_show.ProperName), + episodeproperattr = HttpUtility.HtmlAttributeEncode(_episode.ProperName), + episodeproper = HttpUtility.HtmlEncode(_episode.ProperName), + episodename = HttpUtility.HtmlEncode(_episode.EpisodeName), + episodedescription = DescriptLinkUtils(_episode.Description ?? "").Replace("\n", "
    ") + }; + } + else + { + value = new + { + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasmovie = false + }; + } + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEpisode.RenderAsync(value))); + + } + + private async Task UploadEpisodeStreamAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + string seasonS = seasonPathValueServer.GetValue(ctx); + string episodeS = episodePathValueServer.GetValue(ctx); + if (!int.TryParse(seasonS, out var season)) + season = 1; + if (!int.TryParse(episodeS, out var episode)) + episode = 1; + + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (me != null) + { + + Directory.CreateDirectory(Path.Combine(path, user, "show", show)); + var tmpFile = Path.Combine(path, user, "show", show, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + foreach (var item in ctx.ParseBody((n, fn, ct) => File.Create(tmpFile))) + item.Value.Dispose(); + var _episode = provider.GetEpisode(user, show, season, episode); + if (_episode != null) + { + if (ctx.QueryParams.TryGetFirst("type", out var type)) + { + switch (type) + { + case "thumbnail": + string thumb = Path.Combine(path, user, "show", show, $"Season {season.ToString("D2")}", $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}-thumbnail.jpg"); + if (File.Exists(thumb)) File.Delete(thumb); + File.Move(tmpFile, thumb); + break; + case "poster": + string poster = Path.Combine(path, user, "show", show, $"Season {season.ToString("D2")}", $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}-poster.jpg"); + if (File.Exists(poster)) File.Delete(poster); + File.Move(tmpFile, poster); + break; + case "movie": + string movie = Path.Combine(path, user, "show", show, $"Season {season.ToString("D2")}", $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}.mp4"); + if (File.Exists(movie)) File.Delete(movie); + File.Move(tmpFile, movie); + ScheduleFFmpeg($"-y -i \"{movie}\" {Configuration.BrowserTranscode} \"{Path.Combine(path, user, "show", show, $"Season {season.ToString("D2")}", $"S{season.ToString("D2")}E{episode.ToString("D2")}.mp4")}\""); + break; + + } + ScheduleTask(async () => + { + await GenerateBittorentFileShowAsync(user, show); + }); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); + return; + } + + } + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + } private async Task AddEpisodeAsync(ServerContext ctx) { ctx.ParseBody(); - string user=usersPathValueServer.GetValue(ctx); - string show=showPathValueServer.GetValue(ctx); + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); string season = seasonPathValueServer.GetValue(ctx); var me = GetAccount(ctx); - var _show = provider.GetShow(user,show); - + var _show = provider.GetShow(user, show); + int seasonNo = 1; - if(!int.TryParse(season,out seasonNo)) + if (!int.TryParse(season, out seasonNo)) seasonNo = 1; - if(me != null && me.Username != user && !me.IsAdmin) + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - int episode=1; - if(ctx.QueryParams.TryGetFirst("number",out var number) && !int.TryParse(number,out episode)) - episode=1; - if(ctx.QueryParams.TryGetFirst("proper_name",out var proper_name) && ctx.QueryParams.TryGetFirst("description",out var description) && ctx.QueryParams.TryGetFirst("name",out var name)) + int episode = 1; + if (ctx.QueryParams.TryGetFirst("number", out var number) && !int.TryParse(number, out episode)) + episode = 1; + if (ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("description", out var description) && ctx.QueryParams.TryGetFirst("name", out var name)) { - provider.CreateEpisode(user,show,seasonNo,episode,name,proper_name,description); + provider.CreateEpisode(user, show, seasonNo, episode, name, proper_name, description); await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/show/{show}/season/{seasonNo}/episode/{episode}/edit"); } else { - + } } } - - private Task EditSeasonPagePostAsync(ServerContext ctx) + private async Task EditSeasonPagePostAsync(ServerContext ctx) { - throw new NotImplementedException(); + ctx.ParseBody(); + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + string seasonS = seasonPathValueServer.GetValue(ctx); + if (!int.TryParse(seasonS, out var season)) + season = 1; + + var me = GetAccount(ctx,true); + var _season = provider.GetSeason(user, show, season); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + if (_season != null) + { + if (ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("description", out var description)) + { + _season.ProperName = proper_name; + _season.Description = description.Replace("\r", ""); + provider.UpdateSeason(_season); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); + return; + } + } + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + + } + public async Task EditEpisodePageAsync(ServerContext ctx) + { + + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + var me = GetAccount(ctx); + string seasonS = seasonPathValueServer.GetValue(ctx); + string episodeS = episodePathValueServer.GetValue(ctx); + if (!int.TryParse(seasonS, out var season)) + season = 1; + if (!int.TryParse(episodeS, out var episode)) + episode = 1; + var _episode = provider.GetEpisode(user, show, season, episode); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + if (_episode != null) + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditEpisodeDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_episode.ProperName), Description = System.Web.HttpUtility.HtmlEncode(_episode.Description) }))); + } + else + { + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    You are unauthorized to edit this

    ")); + } + + } + + private async Task EditEpisodePagePostAsync(ServerContext ctx) + { + ctx.ParseBody(); + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + string seasonS = seasonPathValueServer.GetValue(ctx); + string episodeS = episodePathValueServer.GetValue(ctx); + if (!int.TryParse(seasonS, out var season)) + season = 1; + if (!int.TryParse(episodeS, out var episode)) + episode = 1; + + var me = GetAccount(ctx,true); + var _episode = provider.GetEpisode(user, show, season, episode); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + if (_episode != null) + { + if (ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("description", out var description)) + { + _episode.ProperName = proper_name; + _episode.Description = description.Replace("\r", ""); + provider.UpdateEpisode(_episode); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); + return; + } + } + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + } private async Task EditSeasonPageAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string show=showPathValueServer.GetValue(ctx); + + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); string seasonS = seasonPathValueServer.GetValue(ctx); int season = 1; - if(!int.TryParse(seasonS,out season)) - season=1; + if (!int.TryParse(seasonS, out season)) + season = 1; var me = GetAccount(ctx); - - var _season = provider.GetSeason(user,show,season); - if(me != null && me.Username != user && !me.IsAdmin) + var _season = provider.GetSeason(user, show, season); + + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - if(_season != null) - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageEditSeasonDetails.RenderAsync(new{Propername=System.Web.HttpUtility.HtmlAttributeEncode( _season.ProperName),newseasonnumber = provider.EpisodeCount(user,show,season)+1,Description=System.Web.HttpUtility.HtmlEncode(_season.Description)}))); + if (_season != null) + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditSeasonDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_season.ProperName), newseasonnumber = provider.EpisodeCount(user, show, season) + 1, Description = System.Web.HttpUtility.HtmlEncode(_season.Description) }))); } else { - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You are unauthorized to edit this

    ")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    You are unauthorized to edit this

    ")); } } - + private async Task SeasonPageAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string show=showPathValueServer.GetValue(ctx); + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); string season = seasonPathValueServer.GetValue(ctx); - - var _show= provider.GetShow(user,show); - var _user = provider.GetUserAccount(user); - if(!int.TryParse(season,out var seasonNo)) - seasonNo=1; - var _season = provider.GetSeason(user,show,seasonNo); - var me = GetAccount(ctx); - - if(me != null && me.Username != user && !me.IsAdmin) + var _show = provider.GetShow(user, show); + var _user = provider.GetUserAccount(user); + if (!int.TryParse(season, out var seasonNo)) + seasonNo = 1; + var _season = provider.GetSeason(user, show, seasonNo); + + var me = GetAccount(ctx); + + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - object value; - if(_show != null && _user != null && _season != null) - { - string showDir = Path.Combine(this.path,user,"show",show); - - string thumb = File.Exists(Path.Combine(showDir,user,"show",show,$"Season {_season.SeasonNumber.ToString("D2")}","thumbnail.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{_season.SeasonNumber.ToString("D2")}/thumbnail.jpg" : $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"; - List episodes=new List(); - for(int i = 1;i<=provider.EpisodeCount(user,show,seasonNo);i++) - { - var item = provider.GetEpisode(user,show,seasonNo,i); - if(item != null) - //var data = GetMovieContentMetaData(Configuration,user,item.Name); - episodes.Add(item.Scriban(Configuration,path,user,show)); - } - value = new{ episodes, editable=me!=null, userproper=HttpUtility.HtmlEncode(_user.ProperName), username=HttpUtility.HtmlEncode(user), rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title,hasshow=true,seasonthumbnail=thumb,seasonproper=HttpUtility.HtmlEncode(_season.ProperName),showproper=HttpUtility.HtmlEncode(_show.ProperName),showname=HttpUtility.HtmlEncode(_show.Name),seasondescription=DescriptLinkUtils(_season.Description ?? "").Replace("\n","
    ")}; - } - else - { - value = new{ username=user, rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title,hasshow=false}; - } + object value; + if (_show != null && _user != null && _season != null) + { + string showDir = Path.Combine(this.path, user, "show", show); + + string thumb = File.Exists(Path.Combine(showDir, $"Season {_season.SeasonNumber.ToString("D2")}", "thumbnail.jpg")) ? $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{_season.SeasonNumber.ToString("D2")}/thumbnail.jpg" : $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"; + List episodes = new List(); + for (int i = 1; i <= provider.EpisodeCount(user, show, seasonNo); i++) + { + var item = provider.GetEpisode(user, show, seasonNo, i); + if (item != null) + //var data = GetMovieContentMetaData(Configuration,user,item.Name); + episodes.Add(item.Scriban(Configuration, path, user, show)); + } + value = new + { + episodes, + editable = me != null, + userproper = HttpUtility.HtmlEncode(_user.ProperName), + username = HttpUtility.HtmlEncode(user), + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasshow = true, + seasonthumbnail = thumb, + seasonproper = HttpUtility.HtmlEncode(_season.ProperName), + showproper = HttpUtility.HtmlEncode(_show.ProperName), + showname = HttpUtility.HtmlEncode(_show.Name), + seasondescription = DescriptLinkUtils(_season.Description ?? "").Replace("\n", "
    ") + }; + } + else + { + value = new + { + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasshow = false + }; + } + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSeason.RenderAsync(value))); - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageSeason.RenderAsync(value))); - } private async Task AddSeasonAsync(ServerContext ctx) { ctx.ParseBody(); - string user=usersPathValueServer.GetValue(ctx); - string show=showPathValueServer.GetValue(ctx); + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); var me = GetAccount(ctx); - var _show = provider.GetShow(user,show); + var _show = provider.GetShow(user, show); - if(me != null && me.Username != user && !me.IsAdmin) + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { int season = 1; - if(ctx.QueryParams.TryGetFirst("number",out var number) && !int.TryParse(number,out season)) - season=1; - if(ctx.QueryParams.TryGetFirst("proper_name",out var proper_name) && ctx.QueryParams.TryGetFirst("description",out var description)) + if (ctx.QueryParams.TryGetFirst("number", out var number) && !int.TryParse(number, out season)) + season = 1; + if (ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("description", out var description)) { - provider.CreateSeason(user,show,season,proper_name,description); + provider.CreateSeason(user, show, season, proper_name, description); await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/show/{show}/season/{season}/edit"); } else { } - } + } } - private Task EditShowPagePostAsync(ServerContext ctx) + private async Task EditShowPagePostAsync(ServerContext ctx) { - throw new NotImplementedException(); + ctx.ParseBody(); + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _show = provider.GetShow(user, show); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + if (_show != null) + { + if (ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("description", out var description)) + { + _show.ProperName = proper_name; + _show.Description = description.Replace("\r", ""); + provider.UpdateShow(_show); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); + return; + } + } + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + } private async Task EditShowPageAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string show=showPathValueServer.GetValue(ctx); + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); var me = GetAccount(ctx); - var _show = provider.GetShow(user,show); + var _show = provider.GetShow(user, show); - if(me != null && me.Username != user && !me.IsAdmin) + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - if(_show != null) - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageEditShowDetails.RenderAsync(new{Propername=System.Web.HttpUtility.HtmlAttributeEncode( _show.ProperName),newseasonnumber = provider.SeasonCount(user,show)+1,Description=System.Web.HttpUtility.HtmlEncode(_show.Description)}))); + if (_show != null) + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditShowDetails.RenderAsync(new { Propername = System.Web.HttpUtility.HtmlAttributeEncode(_show.ProperName), newseasonnumber = provider.SeasonCount(user, show) + 1, Description = System.Web.HttpUtility.HtmlEncode(_show.Description) }))); } else { - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You are unauthorized to edit this

    ")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    You are unauthorized to edit this

    ")); } } private async Task ShowPageAsync(ServerContext ctx) { - - string user=usersPathValueServer.GetValue(ctx); - string show=showPathValueServer.GetValue(ctx); - - var _show= provider.GetShow(user,show); - var _user = provider.GetUserAccount(user); - var me = GetAccount(ctx); - - if(me != null && me.Username != user && !me.IsAdmin) + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + + var _show = provider.GetShow(user, show); + var _user = provider.GetUserAccount(user); + + var me = GetAccount(ctx,out var cookie); + + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - object value; - if(_show != null && _user != null) - { - string showDir = Path.Combine(this.path,user,"show",show); - bool torrent= File.Exists(Path.Combine(showDir,$"{show}.torrent")); - bool torrent_wextra= File.Exists(Path.Combine(showDir,$"{show}_withextras.torrent")); - bool extrasexists = Directory.Exists(Path.Combine(showDir,"extras")) || me != null; - - string thumb = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"; - List seasons=new List(); - for(int i = 1;i<=provider.SeasonCount(user,show);i++) - { - var item = provider.GetSeason(user,show,i); - if(item != null) - //var data = GetMovieContentMetaData(Configuration,user,item.Name); - seasons.Add(item.Scriban(Configuration,path,user,show)); - } - value = new{ seasons,extrasexists=extrasexists, torrentexists=torrent, torrentwextraexists=torrent_wextra, torrent=$"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/{show}.torrent",torrentwextra=$"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/{show}_withextras.torrent" , editable=me!=null, userproper=HttpUtility.HtmlEncode(_user.ProperName), username=HttpUtility.HtmlEncode(user), rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title,hasshow=true,showthumbnail=thumb,showproper=HttpUtility.HtmlEncode(_show.ProperName),showname=HttpUtility.HtmlEncode(_show.Name),showdescription=DescriptLinkUtils(_show.Description ?? "").Replace("\n","
    ")}; - } - else - { - value = new{ username=user, rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title,hasshow=false}; - } + object value; + if (_show != null && _user != null) + { + string csrf=""; + if(me != null) + csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + string showDir = Path.Combine(this.path, user, "show", show); + bool torrent = File.Exists(Path.Combine(showDir, $"{show}.torrent")); + bool torrent_wextra = File.Exists(Path.Combine(showDir, $"{show}_withextras.torrent")); + bool extrasexists = Directory.Exists(Path.Combine(showDir, "extras")) || me != null; + + string thumb = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"; + List seasons = new List(); + for (int i = 1; i <= provider.SeasonCount(user, show); i++) + { + var item = provider.GetSeason(user, show, i); + if (item != null) + //var data = GetMovieContentMetaData(Configuration,user,item.Name); + seasons.Add(item.Scriban(Configuration, path, user, show)); + } + value = new + { + csrf, + seasons, + extrasexists, + torrentexists = torrent, + torrentwextraexists = torrent_wextra, + torrent = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/{show}.torrent", + torrentwextra = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/{show}_withextras.torrent", + editable = me != null, + userproper = HttpUtility.HtmlEncode(_user.ProperName), + username = HttpUtility.HtmlEncode(user), + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasshow = true, + showthumbnail = thumb, + showproper = HttpUtility.HtmlEncode(_show.ProperName), + showname = HttpUtility.HtmlEncode(_show.Name), + showdescription = DescriptLinkUtils(_show.Description ?? "").Replace("\n", "
    ") + }; + } + else + { + value = new + { + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasshow = false + }; + } + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageShow.RenderAsync(value))); - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageShow.RenderAsync(value))); - } private IServer Shows() { - RouteServer routeServer=new RouteServer(); - routeServer.Add("/",ShowsAsync); + RouteServer routeServer = new RouteServer(); + routeServer.Add("/", ShowsAsync); return routeServer; } - private async Task ShowsAsync(ServerContext ctx) + private IServer Albums() { - - string user=usersPathValueServer.GetValue(ctx); - List shows=new List(); - foreach(var item in provider.GetShows(user)) - { - //var data = GetMovieContentMetaData(Configuration,user,item.Name); - shows.Add(item.Scriban(Configuration,user)); - } - - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageShows.RenderAsync(new{Shows=shows}))); + RouteServer routeServer = new RouteServer(); + routeServer.Add("/", AlbumsAsync); + return routeServer; } - private bool StartsWithAt(string str,int indexof,string startsWith) + + private async Task ShowsAsync(ServerContext ctx) { - if(str.Length-indexof < startsWith.Length) return false; - for(int i = 0;i shows = new List(); + foreach (var item in provider.GetShows(user)) { - if(str[i+indexof] != startsWith[i]) return false; + //var data = GetMovieContentMetaData(Configuration,user,item.Name); + shows.Add(item.Scriban(Configuration, user)); + } + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageShows.RenderAsync(new { Shows = shows }))); + } + + + private async Task AlbumsAsync(ServerContext ctx) + { + + string user = usersPathValueServer.GetValue(ctx); + List albums = new List(); + foreach (var item in provider.GetAlbums(user)) + { + //var data = GetMovieContentMetaData(Configuration,user,item.Name); + string thumb = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{item.Name}/thumbnail.jpg"; + albums.Add(item.Scriban(thumb)); + } + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageAlbums.RenderAsync(new { Albums = albums }))); + } + + private static bool StartsWithAt(string str, int indexof, string startsWith) + { + if (str.Length - indexof < startsWith.Length) return false; + for (int i = 0; i < startsWith.Length; i++) + { + if (str[i + indexof] != startsWith[i]) return false; } return true; } - private string DescriptLinkUtils(string url) + internal static string DescriptLinkUtils(string url) { - StringBuilder b=new StringBuilder(); - for(int i = 0;i")}))); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await aboutUser.RenderAsync(new { Propername = HttpUtility.HtmlEncode(accountData.ProperName), Aboutme = DescriptLinkUtils(accountData.AboutMe ?? "").Replace("\n", "
    ") }))); } } private async Task UserPageAsync(ServerContext ctx) { - await ctx.SendTextAsync(await RenderHtmlAsync(false,await AssetProvider.ReadAllTextAsync("/UserPage.html"))); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await AssetProvider.ReadAllTextAsync("/UserPage.html"))); } - ConcurrentQueue> tasks=new ConcurrentQueue>(); + ConcurrentQueue> tasks = new ConcurrentQueue>(); private void RegisterMoviePath() { - movieRouteServer.Add("/",MoviePageAsync); - movieRouteServer.Add("/play",PlayMoviePageAsync); - movieRouteServer.Add("/edit",EditMoviePageAsync); - movieRouteServer.Add("/edit",EditMoviePagePostAsync,"POST"); - movieRouteServer.Add("/upload",UploadMovieStreamAsync,"POST"); - movieRouteServer.Add("/upload_extra",UploadMovieExtraAsync,"POST"); - movieRouteServer.Add("/extras",ExtrasMoviePageAsync); - movieRouteServer.Add("/mkdir",ExtrasMovieMkdirAsync,"POST"); - movieRouteServer.Add("/subtitles",SubtitlesMovieAsync); - movieRouteServer.Add("/subtitles",SubtitlesMoviePostAsync,"POST"); + movieRouteServer.Add("/", MoviePageAsync); + movieRouteServer.Add("/play", PlayMoviePageAsync); + movieRouteServer.Add("/edit", EditMoviePageAsync); + movieRouteServer.Add("/sendupdate", SendMovieUpdateAsync, "POST"); + movieRouteServer.Add("/edit", EditMoviePagePostAsync, "POST"); + movieRouteServer.Add("/upload", UploadMovieStreamAsync, "POST"); + movieRouteServer.Add("/upload_extra", UploadMovieExtraAsync, "POST"); + movieRouteServer.Add("/extras", ExtrasMoviePageAsync); + movieRouteServer.Add("/mkdir", ExtrasMovieMkdirAsync, "POST"); + movieRouteServer.Add("/subtitles", SubtitlesMovieAsync); + movieRouteServer.Add("/subtitles", SubtitlesMoviePostAsync, "POST"); //http://192.168.0.158:62444/user/tesses/movie/MyGreatMovie/mkdir } + private async Task SendShowUpdateAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _user = provider.GetUserAccount(user); + var _show = provider.GetShow(user, show); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + ctx.ParseBody(); + if (!ctx.QueryParams.TryGetFirst("body", out var body)) + { + body = ""; + } + + ScheduleTask(async () => + { + await Creator.EmailShowAsync(provider, _user, _show, true, body); + await SendEvent(EventType.ShowUpdate, _user.ProperName, _user.Username, _show.Name, _show.ProperName, _show.Description, body); + + }); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/show/{show}/"); + } + else + { + await ctx.SendTextAsync("Failed to send update"); + } + } + private async Task SendMovieUpdateAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _user = provider.GetUserAccount(user); + var _movie = provider.GetMovie(user, movie); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + ctx.ParseBody(); + if (!ctx.QueryParams.TryGetFirst("body", out var body)) + { + body = ""; + } + ScheduleTask(async () => + { + await Creator.EmailMovieAsync(provider, _user, _movie, true, body); + await SendEvent(EventType.MovieUpdate, _user.ProperName, _user.Username, _movie.Name, _movie.ProperName, _movie.Description, body); + }); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/movie/{movie}/"); + } + else + { + await ctx.SendTextAsync("Failed to send update"); + } + } + private async Task SendAlbumUpdateAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _user = provider.GetUserAccount(user); + var _album = provider.GetAlbum(user, album); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + ctx.ParseBody(); + if (!ctx.QueryParams.TryGetFirst("body", out var body)) + { + body = ""; + } + ScheduleTask(async () => + { + await Creator.EmailAlbumAsync(provider, _user, _album, true, body); + await SendEvent(EventType.AlbumUpdate, _user.ProperName, _user.Username, _album.Name, _album.ProperName, _album.Description, body); + }); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/album/{album}/"); + } + else + { + await ctx.SendTextAsync("Failed to send update"); + } + } private async Task SubtitlesMoviePostAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - var _movie = provider.GetMovie(user,movie); - if(me != null && me.Username != user && !me.IsAdmin) + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _movie = provider.GetMovie(user, movie); + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - - if(me != null) - { - if(ctx.QueryParams.TryGetFirst("lang",out var lang) && !string.IsNullOrWhiteSpace(lang)) + + if (me != null) + { + if (ctx.QueryParams.TryGetFirst("lang", out var lang) && !string.IsNullOrWhiteSpace(lang)) { var json = await ctx.ReadJsonAsync>(); - string langDir = Path.Combine(path,user,"movie",movie,"subtitles",lang); + string langDir = Path.Combine(path, user, "movie", movie, "subtitles", lang); Directory.CreateDirectory(langDir); - string langFile = Path.Combine(langDir,$"{movie}.json"); - string vtt = Path.Combine(langDir,$"{movie}.vtt"); - string srt = Path.Combine(langDir,$"{movie}.srt"); - File.WriteAllText(langFile,JsonConvert.SerializeObject(json)); - using(var vttFile=File.CreateText(vtt)) + string langFile = Path.Combine(langDir, $"{movie}.json"); + string vtt = Path.Combine(langDir, $"{movie}.vtt"); + string srt = Path.Combine(langDir, $"{movie}.srt"); + File.WriteAllText(langFile, JsonConvert.SerializeObject(json)); + using (var vttFile = File.CreateText(vtt)) { - Subtitle.ToWebVTT(vttFile,json); + Subtitle.ToWebVTT(vttFile, json); } - using(var srtFile=File.CreateText(srt)) + using (var srtFile = File.CreateText(srt)) { - Subtitle.ToSrt(srtFile,json); + Subtitle.ToSrt(srtFile, json); } await ctx.SendTextAsync("Success"); return; } } - ctx.StatusCode=400; + ctx.StatusCode = 400; + await ctx.SendTextAsync("Fail"); + } + private async Task SubtitlesEpisodeAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + string seasonS = seasonPathValueServer.GetValue(ctx); + string episodeS = episodePathValueServer.GetValue(ctx); + if (!int.TryParse(seasonS, out var season)) + season = 1; + + if (!int.TryParse(episodeS, out var episode)) + episode = 1; + + var me = GetAccount(ctx); + var _show = provider.GetMovie(user, show); + var _episode = provider.GetEpisode(user, show, season, episode); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (me != null) + { + if (ctx.QueryParams.TryGetFirst("lang", out var lang) && !string.IsNullOrWhiteSpace(lang)) + { + string langDir = Path.Combine(path, user, "show", show, $"Season {season.ToString("D2")}", $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}-subtitles", lang); + string langFile = Path.Combine(langDir, $"{_episode.ProperName} S{season.ToString("D2")}E{episode.ToString("D2")}.json"); + string browserfile = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{season.ToString("D2")}/S{season.ToString("D2")}E{episode.ToString("D2")}.mp4"; + string json = ""; + bool hasjson = false; + if (File.Exists(langFile)) + { + hasjson = true; + json = File.ReadAllText(langFile); + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSubtitleEditor.RenderAsync(new { hasjson, json, lang, browserfile }))); + } + else + { + List languages = new List(); + + foreach (var item in Languages) + { + languages.Add(new { code = item.LangCode, name = item.LangTitle }); + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSubtitleLangList.RenderAsync(new { languages }))); + } + + } + } + + private async Task SubtitlesEpisodePostAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + string seasonS = seasonPathValueServer.GetValue(ctx); + string episodeS = episodePathValueServer.GetValue(ctx); + if (!int.TryParse(seasonS, out var season)) + season = 1; + + if (!int.TryParse(episodeS, out var episode)) + episode = 1; + + var me = GetAccount(ctx,true); + var _show = provider.GetMovie(user, show); + var _episode = provider.GetEpisode(user, show, season, episode); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + if (ctx.QueryParams.TryGetFirst("lang", out var lang) && !string.IsNullOrWhiteSpace(lang)) + { + var json = await ctx.ReadJsonAsync>(); + string langDir = Path.Combine(path, user, "show", show, $"Season {season.ToString("D2")}", $"{_episode.EpisodeName} S{season.ToString("D2")}E{episode.ToString("D2")}-subtitles", lang); + Directory.CreateDirectory(langDir); + string langFile = Path.Combine(langDir, $"{_episode.ProperName} S{season.ToString("D2")}E{episode.ToString("D2")}.json"); + string vtt = Path.Combine(langDir, $"{_episode.ProperName} S{season.ToString("D2")}E{episode.ToString("D2")}.vtt"); + string srt = Path.Combine(langDir, $"{_episode.ProperName} S{season.ToString("D2")}E{episode.ToString("D2")}.srt"); + File.WriteAllText(langFile, JsonConvert.SerializeObject(json)); + using (var vttFile = File.CreateText(vtt)) + { + Subtitle.ToWebVTT(vttFile, json); + } + using (var srtFile = File.CreateText(srt)) + { + Subtitle.ToSrt(srtFile, json); + } + await ctx.SendTextAsync("Success"); + return; + } + } + ctx.StatusCode = 400; await ctx.SendTextAsync("Fail"); } private async Task SubtitlesMovieAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); var me = GetAccount(ctx); - var _movie = provider.GetMovie(user,movie); - if(me != null && me.Username != user && !me.IsAdmin) + var _movie = provider.GetMovie(user, movie); + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) - { if(ctx.QueryParams.TryGetFirst("lang",out var lang) && !string.IsNullOrWhiteSpace(lang)) + if (me != null) { - string langDir = Path.Combine(path,user,"movie",movie,"subtitles",lang); - string langFile = Path.Combine(langDir,$"{movie}.json"); - string browserfile = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/browser.mp4"; - string json=""; - bool hasjson=false; - if(File.Exists(langFile)) + if (ctx.QueryParams.TryGetFirst("lang", out var lang) && !string.IsNullOrWhiteSpace(lang)) { - hasjson=true; - json=File.ReadAllText(langFile); + string langDir = Path.Combine(path, user, "movie", movie, "subtitles", lang); + string langFile = Path.Combine(langDir, $"{movie}.json"); + string browserfile = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/browser.mp4"; + string json = ""; + bool hasjson = false; + if (File.Exists(langFile)) + { + hasjson = true; + json = File.ReadAllText(langFile); + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSubtitleEditor.RenderAsync(new { hasjson, json, lang, browserfile }))); } - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageSubtitleEditor.RenderAsync(new{hasjson,json,lang,browserfile}))); - } - else - { - List languages = new List(); + else + { + List languages = new List(); - foreach(var item in Languages) - { - languages.Add(new {code=item.LangCode, name=item.LangTitle}); + foreach (var item in Languages) + { + languages.Add(new { code = item.LangCode, name = item.LangTitle }); + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageSubtitleLangList.RenderAsync(new { languages }))); } - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageSubtitleLangList.RenderAsync(new {languages}))); - } } } + private async Task UploadAlbumExtraAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (me != null) + { + Directory.CreateDirectory(Path.Combine(path, user, "album", album)); + var tmpFile = Path.Combine(path, user, "album", album, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + string filename = ""; + foreach (var item in ctx.ParseBody((n, fn, ct) => { filename = fn; return File.Create(tmpFile); })) + item.Value.Dispose(); + + if (ctx.QueryParams.TryGetFirst("parent", out var parent)) + { + var _path = SanitizePath($"{parent.TrimStart('/').TrimEnd('/')}/{filename}").TrimStart('/'); + var _path2 = Path.Combine(path, user, "album", album, "extras", _path); + if (File.Exists(_path2)) + File.Delete(_path2); + File.Move(tmpFile, _path2); + ScheduleTask(async () => + { + await GenerateBittorentFileAlbumAsync(user, album); + }); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/album/{album}/extras?path={System.Web.HttpUtility.UrlEncode(parent)}"); + } + + } + } + private async Task UploadMovieExtraAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - var _movie = provider.GetMovie(user,movie); - if(me != null && me.Username != user && !me.IsAdmin) + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - Directory.CreateDirectory(Path.Combine(path,user,"movie",movie)); - var tmpFile = Path.Combine(path,user,"movie",movie,$"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); - string filename=""; - foreach(var item in ctx.ParseBody((n,fn,ct)=>{ filename=fn; return File.Create(tmpFile);})) + Directory.CreateDirectory(Path.Combine(path, user, "movie", movie)); + var tmpFile = Path.Combine(path, user, "movie", movie, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + string filename = ""; + foreach (var item in ctx.ParseBody((n, fn, ct) => { filename = fn; return File.Create(tmpFile); })) item.Value.Dispose(); - - if(ctx.QueryParams.TryGetFirst("parent",out var parent)) + + if (ctx.QueryParams.TryGetFirst("parent", out var parent)) { var _path = SanitizePath($"{parent.TrimStart('/').TrimEnd('/')}/{filename}").TrimStart('/'); - var _path2=Path.Combine(path,user,"movie",movie,"extras",_path); - if(File.Exists(_path2)) + var _path2 = Path.Combine(path, user, "movie", movie, "extras", _path); + if (File.Exists(_path2)) File.Delete(_path2); - File.Move(tmpFile,_path2); - ScheduleTask(async()=>{ - await GenerateBittorentFileMovieAsync(user,movie); + File.Move(tmpFile, _path2); + ScheduleTask(async () => + { + await GenerateBittorentFileMovieAsync(user, movie); }); await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/movie/{movie}/extras?path={System.Web.HttpUtility.UrlEncode(parent)}"); } - + + } + } + private async Task UploadShowExtraAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (me != null) + { + Directory.CreateDirectory(Path.Combine(path, user, "show", show)); + var tmpFile = Path.Combine(path, user, "show", show, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + string filename = ""; + foreach (var item in ctx.ParseBody((n, fn, ct) => { filename = fn; return File.Create(tmpFile); })) + item.Value.Dispose(); + + if (ctx.QueryParams.TryGetFirst("parent", out var parent)) + { + var _path = SanitizePath($"{parent.TrimStart('/').TrimEnd('/')}/{filename}").TrimStart('/'); + var _path2 = Path.Combine(path, user, "show", show, "extras", _path); + if (File.Exists(_path2)) + File.Delete(_path2); + File.Move(tmpFile, _path2); + ScheduleTask(async () => + { + await GenerateBittorentFileShowAsync(user, show); + }); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/show/{show}/extras?path={System.Web.HttpUtility.UrlEncode(parent)}"); + } + } } + private async Task ExtrasAlbumMkdirAsync(ServerContext ctx) + { + ctx.ParseBody(); + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (ctx.QueryParams.TryGetFirst("parent", out var parent) && ctx.QueryParams.TryGetFirst("name", out var name)) + { + var _path = SanitizePath($"{parent.TrimStart('/').TrimEnd('/')}/{name}").TrimStart('/'); + var _path2 = Path.Combine(path, user, "album", album, "extras", _path); + + + if (me != null) + { + Directory.CreateDirectory(_path2); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/album/{album}/extras?path={System.Web.HttpUtility.UrlEncode(_path)}"); + } + else + { + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/album/{album}/extras?path={System.Web.HttpUtility.UrlEncode(parent)}"); + } + } + } private async Task ExtrasMovieMkdirAsync(ServerContext ctx) { ctx.ParseBody(); - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - var _movie = provider.GetMovie(user,movie); + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); - if(me != null && me.Username != user && !me.IsAdmin) + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(ctx.QueryParams.TryGetFirst("parent",out var parent) && ctx.QueryParams.TryGetFirst("name",out var name)) + if (ctx.QueryParams.TryGetFirst("parent", out var parent) && ctx.QueryParams.TryGetFirst("name", out var name)) { var _path = SanitizePath($"{parent.TrimStart('/').TrimEnd('/')}/{name}").TrimStart('/'); - var _path2=Path.Combine(path,user,"movie",movie,"extras",_path); - + var _path2 = Path.Combine(path, user, "movie", movie, "extras", _path); - if(me != null) + + if (me != null) { Directory.CreateDirectory(_path2); await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/movie/{movie}/extras?path={System.Web.HttpUtility.UrlEncode(_path)}"); @@ -1351,89 +2942,236 @@ namespace Tesses.CMS } } } - - private async Task ExtrasMoviePageAsync(ServerContext ctx) + private async Task ExtrasShowMkdirAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - var _movie = provider.GetMovie(user,movie); + ctx.ParseBody(); + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); - if(me != null && me.Username != user && !me.IsAdmin) + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(!ctx.QueryParams.TryGetFirst("path",out var __path)) + if (ctx.QueryParams.TryGetFirst("parent", out var parent) && ctx.QueryParams.TryGetFirst("name", out var name)) { - __path="/"; + var _path = SanitizePath($"{parent.TrimStart('/').TrimEnd('/')}/{name}").TrimStart('/'); + var _path2 = Path.Combine(path, user, "show", show, "extras", _path); + + + if (me != null) + { + Directory.CreateDirectory(_path2); + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/show/{show}/extras?path={System.Web.HttpUtility.UrlEncode(_path)}"); + } + else + { + await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{user}/show/{show}/extras?path={System.Web.HttpUtility.UrlEncode(parent)}"); + } + } + } + + public async Task ExtrasShowPageAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + var me = GetAccount(ctx); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (!ctx.QueryParams.TryGetFirst("path", out var __path)) + { + __path = "/"; } __path = __path.TrimStart('/'); - __path=SanitizePath(__path); - string extrasPath = Path.Combine(path,user,"movie",movie,"extras"); - - if(__path.Length > 0) + __path = SanitizePath(__path); + string extrasPath = Path.Combine(path, user, "show", show, "extras"); + + if (__path.Length > 0) { - extrasPath=Path.Combine(extrasPath,__path); + extrasPath = Path.Combine(extrasPath, __path); } - List paths=new List(); - if(__path.Length > 0) + List paths = new List(); + if (__path.Length > 0) { string up = Path.GetDirectoryName($"/{__path}").TrimStart('/'); - - string path = $"./extras?path=/{System.Web.HttpUtility.UrlEncode(up)}"; - paths.Add(new {Path=path, Type="[PARENT]", Name="Up"}); + + string path = $"./extras?path={System.Web.HttpUtility.UrlEncode(up)}"; + paths.Add(new { Path = path, Type = "[PARENT]", Name = "Up" }); } - else { - paths.Add(new {Path="./",Type="[PARENT]", Name="Up"}); - } - - foreach(var dir in Directory.EnumerateDirectories(extrasPath)) + else { - string dirname = Path.GetFileName(dir); - string dirname_html = System.Web.HttpUtility.HtmlEncode(dirname); - string path = $"./extras?path=/{System.Web.HttpUtility.UrlEncode(__path.TrimEnd('/') + '/' + dirname)}"; - paths.Add(new {Path=path, Type="[DIR]", Name=dirname_html}); + paths.Add(new { Path = "./", Type = "[PARENT]", Name = "Up" }); } - foreach(var file in Directory.EnumerateFiles(extrasPath)) + if (Directory.Exists(extrasPath)) + foreach (var dir in Directory.EnumerateDirectories(extrasPath)) + { + string dirname = Path.GetFileName(dir); + string dirname_html = System.Web.HttpUtility.HtmlEncode(dirname); + string path = $"./extras?path={System.Web.HttpUtility.UrlEncode(__path.TrimEnd('/') + '/' + dirname)}"; + paths.Add(new { Path = path, Type = "[DIR]", Name = dirname_html }); + } + if (Directory.Exists(extrasPath)) + foreach (var file in Directory.EnumerateFiles(extrasPath)) + { + string filename = Path.GetFileName(file); + string filename_html = System.Web.HttpUtility.HtmlEncode(filename); + string path = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/extras/{System.Web.HttpUtility.UrlPathEncode(__path.TrimEnd('/') + '/' + filename)}"; + paths.Add(new { Path = path, Type = "[FILE]", Name = filename_html }); + } + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageExtrasViewer.RenderAsync(new { Extras = paths, Path = $"/{__path}", Parent = __path, Editable = me != null }))); + } + private async Task ExtrasAlbumPageAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,out var cookie); + + if (me != null && me.Username != user && !me.IsAdmin) { - string filename = Path.GetFileName(file); - string filename_html = System.Web.HttpUtility.HtmlEncode(filename); - string path = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/extras/{System.Web.HttpUtility.UrlPathEncode(__path.TrimEnd('/') + '/' + filename)}"; - paths.Add(new {Path=path, Type="[FILE]", Name=filename_html}); + me = null; + } + if (!ctx.QueryParams.TryGetFirst("path", out var __path)) + { + __path = "/"; + } + __path = __path.TrimStart('/'); + + __path = SanitizePath(__path); + string extrasPath = Path.Combine(path, user, "album", album, "extras"); + + if (__path.Length > 0) + { + extrasPath = Path.Combine(extrasPath, __path); } - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageExtrasViewer.RenderAsync(new{Extras=paths,Path=$"/{__path}",Parent=__path,Editable=me != null}))); + List paths = new List(); + if (__path.Length > 0) + { + string up = Path.GetDirectoryName($"/{__path}").TrimStart('/'); + + string path = $"./extras?path={System.Web.HttpUtility.UrlEncode(up)}"; + paths.Add(new { Path = path, Type = "[PARENT]", Name = "Up" }); + } + else + { + paths.Add(new { Path = "./", Type = "[PARENT]", Name = "Up" }); + } + if (Directory.Exists(extrasPath)) + foreach (var dir in Directory.EnumerateDirectories(extrasPath)) + { + string dirname = Path.GetFileName(dir); + string dirname_html = System.Web.HttpUtility.HtmlEncode(dirname); + string path = $"./extras?path={System.Web.HttpUtility.UrlEncode(__path.TrimEnd('/') + '/' + dirname)}"; + paths.Add(new { Path = path, Type = "[DIR]", Name = dirname_html }); + } + if (Directory.Exists(extrasPath)) + foreach (var file in Directory.EnumerateFiles(extrasPath)) + { + string filename = Path.GetFileName(file); + string filename_html = System.Web.HttpUtility.HtmlEncode(filename); + string path = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/{album}/extras/{System.Web.HttpUtility.UrlPathEncode(__path.TrimEnd('/') + '/' + filename)}"; + paths.Add(new { Path = path, Type = "[FILE]", Name = filename_html }); + } + string csrf=""; + if(me != null) + csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageExtrasViewer.RenderAsync(new {csrf, Extras = paths, Path = $"/{__path}", Parent = __path, Editable = me != null }))); + } + + + private async Task ExtrasMoviePageAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + var me = GetAccount(ctx,out var cookie); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (!ctx.QueryParams.TryGetFirst("path", out var __path)) + { + __path = "/"; + } + __path = __path.TrimStart('/'); + + __path = SanitizePath(__path); + string extrasPath = Path.Combine(path, user, "movie", movie, "extras"); + + if (__path.Length > 0) + { + extrasPath = Path.Combine(extrasPath, __path); + } + + List paths = new List(); + if (__path.Length > 0) + { + string up = Path.GetDirectoryName($"/{__path}").TrimStart('/'); + + string path = $"./extras?path={System.Web.HttpUtility.UrlEncode(up)}"; + paths.Add(new { Path = path, Type = "[PARENT]", Name = "Up" }); + } + else + { + paths.Add(new { Path = "./", Type = "[PARENT]", Name = "Up" }); + } + if (Directory.Exists(extrasPath)) + foreach (var dir in Directory.EnumerateDirectories(extrasPath)) + { + string dirname = Path.GetFileName(dir); + string dirname_html = System.Web.HttpUtility.HtmlEncode(dirname); + string path = $"./extras?path={System.Web.HttpUtility.UrlEncode(__path.TrimEnd('/') + '/' + dirname)}"; + paths.Add(new { Path = path, Type = "[DIR]", Name = dirname_html }); + } + if (Directory.Exists(extrasPath)) + foreach (var file in Directory.EnumerateFiles(extrasPath)) + { + string filename = Path.GetFileName(file); + string filename_html = System.Web.HttpUtility.HtmlEncode(filename); + string path = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/extras/{System.Web.HttpUtility.UrlPathEncode(__path.TrimEnd('/') + '/' + filename)}"; + paths.Add(new { Path = path, Type = "[FILE]", Name = filename_html }); + } + string csrf=""; + if(me != null) + csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageExtrasViewer.RenderAsync(new {csrf, Extras = paths, Path = $"/{__path}", Parent = __path, Editable = me != null }))); } private string SanitizePath(string path) { - if(path.Length == 0) return ""; - List paths=new List(); - foreach(var item in path.Replace('\\','/').Split('/')) + if (path.Length == 0) return ""; + List paths = new List(); + foreach (var item in path.Replace('\\', '/').Split('/')) { - if(item == "." || item == "..") continue; + if (item == "." || item == "..") continue; paths.Add(item); } - return string.Join("/",paths); + return string.Join("/", paths); } private class PieceStream : Stream { - public const int PieceLength = 16*1024; - public int CurpieceOffset {get;private set;} =0; + public const int PieceLength = 16 * 1024; + public int CurpieceOffset { get; private set; } = 0; - public List Pieces {get;private set;}=new List(); + public List Pieces { get; private set; } = new List(); public byte[] CalculateCurPiece() { - using(var sha1=SHA1.Create()) + using (var sha1 = SHA1.Create()) { - return sha1.ComputeHash(Curpiece,0,CurpieceOffset); + return sha1.ComputeHash(Curpiece, 0, CurpieceOffset); } } - + public byte[] Curpiece = new byte[PieceLength]; public override bool CanRead => false; @@ -1444,7 +3182,7 @@ namespace Tesses.CMS long len; public override long Length => len; - public bool HasPiece=>CurpieceOffset>0; + public bool HasPiece => CurpieceOffset > 0; public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } @@ -1465,412 +3203,543 @@ namespace Tesses.CMS public override void SetLength(long value) { } - + public override void Write(byte[] buffer, int offset, int count) { - for(int i = 0;i paths = new List(); - if(File.Exists(Path.Combine(movieDir,$"{movie}.mp4"))) - paths.Add(new string[]{$"{movie}.mp4"}); - if(File.Exists(Path.Combine(movieDir,"thumbnail.jpg"))) - paths.Add(new string[]{"thumbnail.jpg"}); - if(File.Exists(Path.Combine(movieDir,"poster.jpg"))) - paths.Add(new string[]{"poster.jpg"}); - if(Directory.Exists(subtitles)) - GetExtras(paths,new string[]{"subtitles"},subtitles); - List lengths = new List(); - foreach(var _path in paths.Select(e=>Path.Combine(e))) - using(var movieStrm = File.OpenRead(Path.Combine(movieDir,_path))) + int track = 1; + foreach (var item in _album.Tracks) { - lengths.Add(movieStrm.Length); - await movieStrm.CopyToAsync(strm); + string name = $"{track.ToString("D2")} {_album.AlbumArtist} - {item}.flac"; + if (File.Exists(Path.Combine(albumDir, name))) paths.Add(new string[] { name }); + track++; } - Torrent torrent =new Torrent(); - torrent.AnnounceList.AddRange(Configuration.BittorrentTrackers); - torrent.CreationDate=DateTime.Now; - torrent.UrlList = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/"; + + if (File.Exists(Path.Combine(albumDir, "thumbnail.jpg"))) + paths.Add(new string[] { "thumbnail.jpg" }); + + List lengths = new List(); + foreach (var _path in paths.Select(e => Path.Combine(e))) + using (var movieStrm = File.OpenRead(Path.Combine(albumDir, _path))) + { + lengths.Add(movieStrm.Length); + await movieStrm.CopyToAsync(strm); + } + Torrent torrent = new Torrent(); + torrent.AnnounceList.AddRange(Configuration.BittorrentTrackers); + torrent.CreationDate = DateTime.Now; + torrent.UrlList = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/"; torrent.Info.PieceLength = PieceStream.PieceLength; - torrent.Info.Name = movie; - foreach(var piece in strm.Pieces) + torrent.Info.Name = album; + foreach (var piece in strm.Pieces) { torrent.Info.Pieces.Add(piece); } - if(strm.HasPiece) torrent.Info.Pieces.Add(strm.CalculateCurPiece()); - for(int i = 0;i paths2 = new List(); - GetExtras(paths2,new string[]{"extras"},extrasDir); - foreach(var _path in paths2.Select(e=>Path.Combine(e))) - using(var movieStrm = File.OpenRead(Path.Combine(movieDir,_path))) - { - lengths.Add(movieStrm.Length); - await movieStrm.CopyToAsync(strm); - } - paths.AddRange(paths2); - torrent=new Torrent(); + GetExtras(paths2, new string[] { "extras" }, extrasDir); + foreach (var _path in paths2.Select(e => Path.Combine(e))) + using (var movieStrm = File.OpenRead(Path.Combine(albumDir, _path))) + { + lengths.Add(movieStrm.Length); + await movieStrm.CopyToAsync(strm); + } + paths.AddRange(paths2); + torrent = new Torrent(); torrent.AnnounceList.AddRange(Configuration.BittorrentTrackers); - torrent.CreationDate=DateTime.Now; + torrent.CreationDate = DateTime.Now; + torrent.UrlList = $"{Configuration.Root.TrimEnd('/')}/content/{user}/album/"; + torrent.Info.PieceLength = PieceStream.PieceLength; + torrent.Info.Name = album; + foreach (var piece in strm.Pieces) + { + torrent.Info.Pieces.Add(piece); + } + if (strm.HasPiece) torrent.Info.Pieces.Add(strm.CalculateCurPiece()); + for (int i = 0; i < paths.Count; i++) + { + var _path = paths[i]; + var len = lengths[i]; + torrent.Info.Files.Add(new TorrentInfoFile() { Path = _path, Length = len }); + } + torrentFile = Path.Combine(albumDir, $"{album}_withextras.torrent"); + using (var file = File.Create(torrentFile)) + { + await torrent.ToTorrentFile().WriteToStreamAsync(file); + } + } + } + + + + private async Task GenerateBittorentFileMovieAsync(string user, string movie) + { + + string movieDir = Path.Combine(this.path, user, "movie", movie); + Directory.CreateDirectory(movieDir); + string extrasDir = Path.Combine(movieDir, "extras"); + string subtitles = Path.Combine(movieDir, "subtitles"); + PieceStream strm = new PieceStream(); + //calculate without extras + List paths = new List(); + if (File.Exists(Path.Combine(movieDir, $"{movie}.mp4"))) + paths.Add(new string[] { $"{movie}.mp4" }); + if (File.Exists(Path.Combine(movieDir, "thumbnail.jpg"))) + paths.Add(new string[] { "thumbnail.jpg" }); + if (File.Exists(Path.Combine(movieDir, "poster.jpg"))) + paths.Add(new string[] { "poster.jpg" }); + if (Directory.Exists(subtitles)) + GetExtras(paths, new string[] { "subtitles" }, subtitles); + List lengths = new List(); + foreach (var _path in paths.Select(e => Path.Combine(e))) + using (var movieStrm = File.OpenRead(Path.Combine(movieDir, _path))) + { + lengths.Add(movieStrm.Length); + await movieStrm.CopyToAsync(strm); + } + Torrent torrent = new Torrent(); + torrent.AnnounceList.AddRange(Configuration.BittorrentTrackers); + torrent.CreationDate = DateTime.Now; torrent.UrlList = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/"; torrent.Info.PieceLength = PieceStream.PieceLength; torrent.Info.Name = movie; - foreach(var piece in strm.Pieces) + foreach (var piece in strm.Pieces) { torrent.Info.Pieces.Add(piece); } - if(strm.HasPiece) torrent.Info.Pieces.Add(strm.CalculateCurPiece()); - for(int i = 0;i paths2 = new List(); + GetExtras(paths2, new string[] { "extras" }, extrasDir); + foreach (var _path in paths2.Select(e => Path.Combine(e))) + using (var movieStrm = File.OpenRead(Path.Combine(movieDir, _path))) + { + lengths.Add(movieStrm.Length); + await movieStrm.CopyToAsync(strm); + } + paths.AddRange(paths2); + torrent = new Torrent(); + torrent.AnnounceList.AddRange(Configuration.BittorrentTrackers); + torrent.CreationDate = DateTime.Now; + torrent.UrlList = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/"; + torrent.Info.PieceLength = PieceStream.PieceLength; + torrent.Info.Name = movie; + foreach (var piece in strm.Pieces) + { + torrent.Info.Pieces.Add(piece); + } + if (strm.HasPiece) torrent.Info.Pieces.Add(strm.CalculateCurPiece()); + for (int i = 0; i < paths.Count; i++) + { + var _path = paths[i]; + var len = lengths[i]; + torrent.Info.Files.Add(new TorrentInfoFile() { Path = _path, Length = len }); + } + torrentFile = Path.Combine(movieDir, $"{movie}_withextras.torrent"); + using (var file = File.Create(torrentFile)) + { + await torrent.ToTorrentFile().WriteToStreamAsync(file); + } } } private void GetExtras(List paths2, string[] torrentPath, string extrasDir) { - foreach(var dir in Directory.GetDirectories(extrasDir)) + foreach (var dir in Directory.GetDirectories(extrasDir)) { string dirname = Path.GetFileName(dir); - string[] path = new string[torrentPath.Length+1]; - Array.Copy(torrentPath,path,torrentPath.Length); - path[path.Length-1] = dirname; - GetExtras(paths2,path,dir); + string[] path = new string[torrentPath.Length + 1]; + Array.Copy(torrentPath, path, torrentPath.Length); + path[path.Length - 1] = dirname; + GetExtras(paths2, path, dir); } - foreach(var file in Directory.GetFiles(extrasDir)) + foreach (var file in Directory.GetFiles(extrasDir)) { string filename = Path.GetFileName(file); - string[] path = new string[torrentPath.Length+1]; - Array.Copy(torrentPath,path,torrentPath.Length); - path[path.Length-1] = filename; + string[] path = new string[torrentPath.Length + 1]; + Array.Copy(torrentPath, path, torrentPath.Length); + path[path.Length - 1] = filename; paths2.Add(path); } } private async Task UploadSeasonStreamAsync(ServerContext ctx) { - - string user=usersPathValueServer.GetValue(ctx); - string show=showPathValueServer.GetValue(ctx); - string season = seasonPathValueServer.GetValue(ctx); - - var _show= provider.GetShow(user,show); - var _user = provider.GetUserAccount(user); - if(!int.TryParse(season,out var seasonNo)) - seasonNo=1; - var _season = provider.GetSeason(user,show,seasonNo); - var me = GetAccount(ctx); - - if(me != null && me.Username != user && !me.IsAdmin) + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + string season = seasonPathValueServer.GetValue(ctx); + + var _show = provider.GetShow(user, show); + var _user = provider.GetUserAccount(user); + if (!int.TryParse(season, out var seasonNo)) + seasonNo = 1; + var _season = provider.GetSeason(user, show, seasonNo); + + var me = GetAccount(ctx,true); + + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - Directory.CreateDirectory(Path.Combine(path,user,"show",show,$"Season {seasonNo}")); - var tmpFile = Path.Combine(path,user,"show",show,$"Season {seasonNo}",$"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); - foreach(var item in ctx.ParseBody((n,fn,ct)=>File.Create(tmpFile))) + Directory.CreateDirectory(Path.Combine(path, user, "show", show, $"Season {seasonNo.ToString("D2")}")); + var tmpFile = Path.Combine(path, user, "show", show, $"Season {seasonNo.ToString("D2")}", $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + foreach (var item in ctx.ParseBody((n, fn, ct) => File.Create(tmpFile))) item.Value.Dispose(); - if(_show != null) - { - if(ctx.QueryParams.TryGetFirst("type",out var type)) + if (_show != null) + { + if (ctx.QueryParams.TryGetFirst("type", out var type)) { - switch(type) + switch (type) { case "thumbnail": - string thumb = Path.Combine(path,user,"show",show,$"Season {seasonNo}","thumbnail.jpg"); - if(File.Exists(thumb)) File.Delete(thumb); - File.Move(tmpFile,thumb); + string thumb = Path.Combine(path, user, "show", show, $"Season {seasonNo.ToString("D2")}", "thumbnail.jpg"); + if (File.Exists(thumb)) File.Delete(thumb); + File.Move(tmpFile, thumb); break; case "poster": - string poster = Path.Combine(path,user,"show",show,$"Season {seasonNo}","poster.jpg"); - if(File.Exists(poster)) File.Delete(poster); - File.Move(tmpFile,poster); + string poster = Path.Combine(path, user, "show", show, $"Season {seasonNo.ToString("D2")}", "poster.jpg"); + if (File.Exists(poster)) File.Delete(poster); + File.Move(tmpFile, poster); break; } - ScheduleTask(async()=>{ - await GenerateBittorentFileShowAsync(user,show); + ScheduleTask(async () => + { + await GenerateBittorentFileShowAsync(user, show); }); - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Success

    <- Back")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); return; } - } + } } - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Failed

    <- Back")); - + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + } private async Task UploadShowStreamAsync(ServerContext ctx) { - - string user=usersPathValueServer.GetValue(ctx); - string show=showPathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - var _show = provider.GetShow(user,show); - if(me != null && me.Username != user && !me.IsAdmin) + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _show = provider.GetShow(user, show); + + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - Directory.CreateDirectory(Path.Combine(path,user,"show",show)); - var tmpFile = Path.Combine(path,user,"show",show,$"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); - foreach(var item in ctx.ParseBody((n,fn,ct)=>File.Create(tmpFile))) + Directory.CreateDirectory(Path.Combine(path, user, "show", show)); + var tmpFile = Path.Combine(path, user, "show", show, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + foreach (var item in ctx.ParseBody((n, fn, ct) => File.Create(tmpFile))) item.Value.Dispose(); - if(_show != null) - { - if(ctx.QueryParams.TryGetFirst("type",out var type)) + if (_show != null) + { + if (ctx.QueryParams.TryGetFirst("type", out var type)) { - switch(type) + switch (type) { case "thumbnail": - string thumb = Path.Combine(path,user,"show",show,"thumbnail.jpg"); - if(File.Exists(thumb)) File.Delete(thumb); - File.Move(tmpFile,thumb); + string thumb = Path.Combine(path, user, "show", show, "thumbnail.jpg"); + if (File.Exists(thumb)) File.Delete(thumb); + File.Move(tmpFile, thumb); break; case "poster": - string poster = Path.Combine(path,user,"show",show,"poster.jpg"); - if(File.Exists(poster)) File.Delete(poster); - File.Move(tmpFile,poster); + string poster = Path.Combine(path, user, "show", show, "poster.jpg"); + if (File.Exists(poster)) File.Delete(poster); + File.Move(tmpFile, poster); break; } - ScheduleTask(async()=>{ - await GenerateBittorentFileShowAsync(user,show); + ScheduleTask(async () => + { + await GenerateBittorentFileShowAsync(user, show); }); - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Success

    <- Back")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); return; } - } + } } - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Failed

    <- Back")); - + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + } private async Task GenerateBittorentFileShowAsync(string user, string show) { - Console.WriteLine("Here"); - string showDir = Path.Combine(this.path,user,"show",show); + string showDir = Path.Combine(this.path, user, "show", show); Directory.CreateDirectory(showDir); - string extrasDir = Path.Combine(showDir,"extras"); - + string extrasDir = Path.Combine(showDir, "extras"); + PieceStream strm = new PieceStream(); //calculate without extras List paths = new List(); - for(int i = 1;i<=provider.SeasonCount(user,show);i++) + for (int i = 1; i <= provider.SeasonCount(user, show); i++) { - var season = provider.GetSeason(user,show,i); - if(season != null) + var season = provider.GetSeason(user, show, i); + if (season != null) { - - string season_name = $"Season {i}"; - if(!Directory.Exists(Path.Combine(showDir,season_name))) continue; - if(File.Exists(Path.Combine(showDir,season_name,"thumbnail.jpg"))) - paths.Add(new string[]{season_name,"thumbnail.jpg"}); - if(File.Exists(Path.Combine(showDir,season_name,"poster.jpg"))) - paths.Add(new string[]{season_name,"poster.jpg"}); - - for(int j = 1;j<=provider.EpisodeCount(user,show,i);j++) + + string season_name = $"Season {i.ToString("D2")}"; + if (!Directory.Exists(Path.Combine(showDir, season_name))) continue; + if (File.Exists(Path.Combine(showDir, season_name, "thumbnail.jpg"))) + paths.Add(new string[] { season_name, "thumbnail.jpg" }); + if (File.Exists(Path.Combine(showDir, season_name, "poster.jpg"))) + paths.Add(new string[] { season_name, "poster.jpg" }); + + for (int j = 1; j <= provider.EpisodeCount(user, show, i); j++) { - var episode = provider.GetEpisode(user,show,i,j); - if(episode != null) + var episode = provider.GetEpisode(user, show, i, j); + if (episode != null) { string name = $"{episode.EpisodeName} S{episode.SeasonNumber.ToString("D2")}E{episode.EpisodeNumber.ToString("D2")}"; string video_name = $"{name}.mp4"; string subtitles_name = $"{name}-subtitles"; string thumbnail = $"{name}-thumbnail.jpg"; string poster = $"{name}-poster.jpg"; - if(File.Exists(Path.Combine(showDir,season_name,poster))) - paths.Add(new string[]{season_name,poster}); - if(File.Exists(Path.Combine(showDir,season_name,thumbnail))) - paths.Add(new string[]{season_name,thumbnail}); - if(File.Exists(Path.Combine(showDir,season_name,video_name))) - paths.Add(new string[]{season_name,video_name}); + if (File.Exists(Path.Combine(showDir, season_name, poster))) + paths.Add(new string[] { season_name, poster }); + if (File.Exists(Path.Combine(showDir, season_name, thumbnail))) + paths.Add(new string[] { season_name, thumbnail }); + if (File.Exists(Path.Combine(showDir, season_name, video_name))) + paths.Add(new string[] { season_name, video_name }); - if(Directory.Exists(Path.Combine(showDir,season_name,subtitles_name))) - GetExtras(paths,new string[]{season_name,subtitles_name},Path.Combine(showDir,season_name,subtitles_name)); + if (Directory.Exists(Path.Combine(showDir, season_name, subtitles_name))) + GetExtras(paths, new string[] { season_name, subtitles_name }, Path.Combine(showDir, season_name, subtitles_name)); } } } } - if(File.Exists(Path.Combine(showDir,"thumbnail.jpg"))) - paths.Add(new string[]{"thumbnail.jpg"}); - if(File.Exists(Path.Combine(showDir,"poster.jpg"))) - paths.Add(new string[]{"poster.jpg"}); - + if (File.Exists(Path.Combine(showDir, "thumbnail.jpg"))) + paths.Add(new string[] { "thumbnail.jpg" }); + if (File.Exists(Path.Combine(showDir, "poster.jpg"))) + paths.Add(new string[] { "poster.jpg" }); + List lengths = new List(); - foreach(var _path in paths.Select(e=>Path.Combine(e))) - using(var movieStrm = File.OpenRead(Path.Combine(showDir,_path))) - { - lengths.Add(movieStrm.Length); - await movieStrm.CopyToAsync(strm); - } - Torrent torrent =new Torrent(); - torrent.AnnounceList.AddRange(Configuration.BittorrentTrackers); - torrent.CreationDate=DateTime.Now; + foreach (var _path in paths.Select(e => Path.Combine(e))) + using (var movieStrm = File.OpenRead(Path.Combine(showDir, _path))) + { + lengths.Add(movieStrm.Length); + await movieStrm.CopyToAsync(strm); + } + Torrent torrent = new Torrent(); + torrent.AnnounceList.AddRange(Configuration.BittorrentTrackers); + torrent.CreationDate = DateTime.Now; torrent.UrlList = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/"; torrent.Info.PieceLength = PieceStream.PieceLength; torrent.Info.Name = show; - foreach(var piece in strm.Pieces) + foreach (var piece in strm.Pieces) { torrent.Info.Pieces.Add(piece); } - if(strm.HasPiece) torrent.Info.Pieces.Add(strm.CalculateCurPiece()); - for(int i = 0;i paths2 = new List(); - GetExtras(paths2,new string[]{"extras"},extrasDir); - foreach(var _path in paths2.Select(e=>Path.Combine(e))) - using(var movieStrm = File.OpenRead(Path.Combine(showDir,_path))) - { - lengths.Add(movieStrm.Length); - await movieStrm.CopyToAsync(strm); - } - paths.AddRange(paths2); - torrent=new Torrent(); + GetExtras(paths2, new string[] { "extras" }, extrasDir); + foreach (var _path in paths2.Select(e => Path.Combine(e))) + using (var movieStrm = File.OpenRead(Path.Combine(showDir, _path))) + { + lengths.Add(movieStrm.Length); + await movieStrm.CopyToAsync(strm); + } + paths.AddRange(paths2); + torrent = new Torrent(); torrent.AnnounceList.AddRange(Configuration.BittorrentTrackers); - torrent.CreationDate=DateTime.Now; - torrent.UrlList = $"{Configuration.Root.TrimEnd('/')}/content/{user}/show/"; - torrent.Info.PieceLength = PieceStream.PieceLength; - torrent.Info.Name = show; - foreach(var piece in strm.Pieces) - { - torrent.Info.Pieces.Add(piece); - } - if(strm.HasPiece) torrent.Info.Pieces.Add(strm.CalculateCurPiece()); - for(int i = 0;i File.Create(tmpFile))) + item.Value.Dispose(); + + if (_movie != null) + { + + string thumb = Path.Combine(path, user, "album", album, "thumbnail.jpg"); + if (File.Exists(thumb)) File.Delete(thumb); + File.Move(tmpFile, thumb); + + + ScheduleTask(async () => + { + await GenerateBittorentFileAlbumAsync(user, album); + }); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); + return; + + + } + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + + } + + private async Task UploadMovieStreamAsync(ServerContext ctx) { - - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - var _movie = provider.GetMovie(user,movie); - if(me != null && me.Username != user && !me.IsAdmin) + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _movie = provider.GetMovie(user, movie); + + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - Directory.CreateDirectory(Path.Combine(path,user,"movie",movie)); - var tmpFile = Path.Combine(path,user,"movie",movie,$"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); - foreach(var item in ctx.ParseBody((n,fn,ct)=>File.Create(tmpFile))) + Directory.CreateDirectory(Path.Combine(path, user, "movie", movie)); + var tmpFile = Path.Combine(path, user, "movie", movie, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + foreach (var item in ctx.ParseBody((n, fn, ct) => File.Create(tmpFile))) item.Value.Dispose(); - if(_movie != null) - { - if(ctx.QueryParams.TryGetFirst("type",out var type)) + if (_movie != null) + { + if (ctx.QueryParams.TryGetFirst("type", out var type)) { - switch(type) + switch (type) { case "thumbnail": - string thumb = Path.Combine(path,user,"movie",movie,"thumbnail.jpg"); - if(File.Exists(thumb)) File.Delete(thumb); - File.Move(tmpFile,thumb); + string thumb = Path.Combine(path, user, "movie", movie, "thumbnail.jpg"); + if (File.Exists(thumb)) File.Delete(thumb); + File.Move(tmpFile, thumb); break; case "poster": - string poster = Path.Combine(path,user,"movie",movie,"poster.jpg"); - if(File.Exists(poster)) File.Delete(poster); - File.Move(tmpFile,poster); + string poster = Path.Combine(path, user, "movie", movie, "poster.jpg"); + if (File.Exists(poster)) File.Delete(poster); + File.Move(tmpFile, poster); break; case "movie": - string mov=Path.Combine(path,user,"movie",movie,$"{movie}.mp4"); - if(File.Exists(mov)) File.Delete(mov); - File.Move(tmpFile,mov); - ScheduleFFmpeg($"-y -i \"{mov}\" {Configuration.BrowserTranscode} \"{Path.Combine(path,user,"movie",movie,"browser.mp4")}\""); + string mov = Path.Combine(path, user, "movie", movie, $"{movie}.mp4"); + if (File.Exists(mov)) File.Delete(mov); + File.Move(tmpFile, mov); + ScheduleFFmpeg($"-y -i \"{mov}\" {Configuration.BrowserTranscode} \"{Path.Combine(path, user, "movie", movie, "browser.mp4")}\""); break; } - ScheduleTask(async()=>{ - await GenerateBittorentFileMovieAsync(user,movie); + ScheduleTask(async () => + { + await GenerateBittorentFileMovieAsync(user, movie); }); - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Success

    <- Back")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); return; } - } + } } - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Failed

    <- Back")); - + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); + } private void ScheduleFFmpeg(string command) { - ScheduleTask(async()=>{ - using(Process process=new Process()) + ScheduleTask(async () => + { + using (Process process = new Process()) { process.StartInfo.Arguments = command; process.StartInfo.FileName = "ffmpeg"; - if(process.Start()) + if (process.Start()) { await Task.Run(process.WaitForExit); } @@ -1886,151 +3755,1101 @@ namespace Tesses.CMS public async Task EditMoviePagePostAsync(ServerContext ctx) { ctx.ParseBody(); - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - var _movie = provider.GetMovie(user,movie); + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + var me = GetAccount(ctx,true); + var _movie = provider.GetMovie(user, movie); - if(me != null && me.Username != user && !me.IsAdmin) + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - if(_movie != null) - { - if(ctx.QueryParams.TryGetFirst("proper_name",out var proper_name) && ctx.QueryParams.TryGetFirst("description",out var description)) + if (_movie != null) + { + if (ctx.QueryParams.TryGetFirst("proper_name", out var proper_name) && ctx.QueryParams.TryGetFirst("description", out var description)) { _movie.ProperName = proper_name; - _movie.Description = description.Replace("\r",""); + _movie.Description = description.Replace("\r", ""); provider.UpdateMovie(_movie); - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Success

    <- Back")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Success

    <- Back")); return; } - } + } } - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    Failed

    <- Back")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    Failed

    <- Back")); } public async Task EditMoviePageAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - var _movie = provider.GetMovie(user,movie); + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + var me = GetAccount(ctx,out var cookie); + var _movie = provider.GetMovie(user, movie); - if(me != null && me.Username != user && !me.IsAdmin) + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - if(me != null) + if (me != null) { - if(_movie != null) - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageEditMovieDetails.RenderAsync(new{Propername=System.Web.HttpUtility.HtmlAttributeEncode( _movie.ProperName),Description=System.Web.HttpUtility.HtmlEncode(_movie.Description)}))); + + if (_movie != null) + { + string csrf=""; + string csrf2=""; + if(me != null) + { + csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + csrf2 = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditMovieDetails.RenderAsync(new {csrf,csrf2, Propername = System.Web.HttpUtility.HtmlAttributeEncode(_movie.ProperName), Description = System.Web.HttpUtility.HtmlEncode(_movie.Description) }))); + } } else { - await ctx.SendTextAsync(await RenderHtmlAsync(false,"

    You are unauthorized to edit this

    ")); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    You are unauthorized to edit this

    ")); } } - private async Task PlayMoviePageAsync(ServerContext ctx) - { - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - - var _movie= provider.GetMovie(user,movie); - object value; - if(_movie != null) - { - var data=GetMovieContentMetaData(Configuration,user,movie); - List subtitles=new List(); - foreach(var subtitle in data.SubtitlesStreams) + public async Task EditAlbumPageAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string album = albumPathValueServer.GetValue(ctx); + var me = GetAccount(ctx,out var cookie); + var _album = provider.GetAlbum(user, album); + + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + + if (me != null) + { + + if (_album != null) + { + string csrf=""; + string csrf2=""; + if(me != null) + { + csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + csrf2 = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + } + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageEditAlbumDetails.RenderAsync(new { Year = _album.Year, Propername = System.Web.HttpUtility.HtmlAttributeEncode(_album.ProperName), Albumartist = System.Web.HttpUtility.HtmlAttributeEncode(_album.AlbumArtist), Description = System.Web.HttpUtility.HtmlEncode(_album.Description) ,csrf,csrf2}))); + } + } + else + { + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, "

    You are unauthorized to edit this

    ")); + } + } + private async Task PlayEpisodePageAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string show = showPathValueServer.GetValue(ctx); + string seasonS = seasonPathValueServer.GetValue(ctx); + string episodeS = episodePathValueServer.GetValue(ctx); + if (!int.TryParse(seasonS, out var season)) season = 1; + if (!int.TryParse(episodeS, out var episode)) episode = 1; + + var _show = provider.GetShow(user, show); + var _episode = provider.GetEpisode(user, show, season, episode); + + + object value; + if (_show != null) + { + + var data = GetEpisodeContentMetaData(user, show, season, episode); + List subtitles = new List(); + foreach (var subtitle in data.SubtitlesStreams) { string languageName = "Unknown"; string langCode = ""; - foreach(var lang in Languages) + foreach (var lang in Languages) { - if(lang.LangCode == subtitle.LanguageCode) + if (lang.LangCode == subtitle.LanguageCode) { - languageName=lang.LangTitle; + languageName = lang.LangTitle; langCode = lang.LangCodeVideo; } } - subtitles.Add(new {langcode=langCode,name=languageName,file=subtitle.VttUrl}); + subtitles.Add(new { langcode = langCode, name = languageName, file = subtitle.VttUrl }); } string thumb = data.ThumbnailUrl; - value = new{ username=user, rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title,hasmovie=true,moviethumbnail=thumb,movieurl=movie,moviename=_movie.Name,moviedescription=_movie.Description,movieposter = data.PosterUrl, moviebrowserurl=data.BrowserStream,subtitles - }; - } - else - { - value = new{ username=user, rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title,hasmovie=false}; - } - await ctx.SendTextAsync(await pageWatchMovie.RenderAsync(value)); - + + value = new + { + curepisode = episode, + curseason = season, + nextepisodeapi = $"{Configuration.Root.TrimEnd('/')}/api/v1/NextEpisode?user={user}&show={show}", + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasmovie = true, + episodethumbnail = thumb, + episodename = _show.Name, + episodeposter = data.PosterUrl, + episodebrowserurl = data.BrowserStream, + subtitles + }; + } + else + { + value = new + { + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasmovie = false + }; + } + + await ctx.SendTextAsync(await pageWatchEpisode.RenderAsync(value)); + + } + private async Task PlayMoviePageAsync(ServerContext ctx) + { + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); + + var _movie = provider.GetMovie(user, movie); + object value; + if (_movie != null) + { + + var data = GetMovieContentMetaData(user, movie); + List subtitles = new List(); + foreach (var subtitle in data.SubtitlesStreams) + { + string languageName = "Unknown"; + string langCode = ""; + foreach (var lang in Languages) + { + if (lang.LangCode == subtitle.LanguageCode) + { + languageName = lang.LangTitle; + langCode = lang.LangCodeVideo; + } + } + subtitles.Add(new { langcode = langCode, name = languageName, file = subtitle.VttUrl }); + } + string thumb = data.ThumbnailUrl; + value = new + { + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasmovie = true, + moviethumbnail = thumb, + movieurl = movie, + moviename = _movie.Name, + moviedescription = _movie.Description, + movieposter = data.PosterUrl, + moviebrowserurl = data.BrowserStream, + subtitles + }; + } + else + { + value = new + { + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasmovie = false + }; + } + + await ctx.SendTextAsync(await pageWatchMovie.RenderAsync(value)); + + } + IEnumerable GetEpisodesFromShows(IEnumerable shows) + { + foreach (var show in shows) + { + foreach (var episode in GetEpisodesFromShow(show)) + yield return episode; + } + } + IEnumerable GetEpisodesFromShow(Show show) + { + var user = provider.GetUserById(show.UserId); + int count = provider.SeasonCount(user.Username, show.Name); + for (int i = 1; i <= count; i++) + { + int count2 = provider.EpisodeCount(user.Username, show.Name, i); + for (int j = 1; j < count2; j++) + { + yield return provider.GetEpisode(user.Username, show.Name, i, j); + } + } + } + IEnumerable GetEpisodesFromSeason(Show show, int season) + { + var user = provider.GetUserById(show.UserId); + int count2 = provider.EpisodeCount(user.Username, show.Name, season); + for (int j = 1; j < count2; j++) + { + yield return provider.GetEpisode(user.Username, show.Name, season, j); + } } private async Task MoviePageAsync(ServerContext ctx) { - string user=usersPathValueServer.GetValue(ctx); - string movie=moviePathValueServer.GetValue(ctx); - - var _movie= provider.GetMovie(user,movie); - var _user = provider.GetUserAccount(user); + string user = usersPathValueServer.GetValue(ctx); + string movie = moviePathValueServer.GetValue(ctx); - var me = GetAccount(ctx); - - if(me != null && me.Username != user && !me.IsAdmin) + var _movie = provider.GetMovie(user, movie); + var _user = provider.GetUserAccount(user); + + var me = GetAccount(ctx,out var cookie); + + if (me != null && me.Username != user && !me.IsAdmin) { - me=null; + me = null; } - object value; - if(_movie != null && _user != null) - { - string movieDir = Path.Combine(this.path,user,"movie",movie); - bool torrent= File.Exists(Path.Combine(movieDir,$"{movie}.torrent")); - bool torrent_wextra= File.Exists(Path.Combine(movieDir,$"{movie}_withextras.torrent")); - bool extrasexists = Directory.Exists(Path.Combine(movieDir,"extras")) || me != null; - bool moviebrowserexists=File.Exists(Path.Combine(movieDir,"browser.mp4")); - bool movieexists=File.Exists(Path.Combine(movieDir,$"{movie}.mp4")); + object value; + if (_movie != null && _user != null) + { + string movieDir = Path.Combine(this.path, user, "movie", movie); + bool torrent = File.Exists(Path.Combine(movieDir, $"{movie}.torrent")); + bool torrent_wextra = File.Exists(Path.Combine(movieDir, $"{movie}_withextras.torrent")); + bool extrasexists = Directory.Exists(Path.Combine(movieDir, "extras")) || me != null; + bool moviebrowserexists = File.Exists(Path.Combine(movieDir, "browser.mp4")); + bool movieexists = File.Exists(Path.Combine(movieDir, $"{movie}.mp4")); string thumb = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/thumbnail.jpg"; - value = new{ extrasexists=extrasexists,moviebrowserexists=moviebrowserexists,movieexists=movieexists, downloadurl=$"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4",torrentexists=torrent, torrentwextraexists=torrent_wextra, torrent=$"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.torrent",torrentwextra=$"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}_withextras.torrent" , editable=me!=null, userproper=HttpUtility.HtmlEncode(_user.ProperName), username=HttpUtility.HtmlEncode(user), rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title,hasmovie=true,moviethumbnail=thumb,movieurl=movie,movieproperattr=HttpUtility.HtmlAttributeEncode(_movie.ProperName),movieproper=HttpUtility.HtmlEncode(_movie.ProperName),moviename=HttpUtility.HtmlEncode(_movie.Name),moviedescription=DescriptLinkUtils(_movie.Description ?? "").Replace("\n","
    ")}; - } - else - { - value = new{ username=user, rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title,hasmovie=false}; - } + string csrf=""; + if(me != null) + csrf = HttpUtility.UrlEncode(CreateCSRF(me.Id,cookie)); + value = new + { + csrf, + extrasexists, + moviebrowserexists, + movieexists, + downloadurl = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4", + torrentexists = torrent, + torrentwextraexists = torrent_wextra, + torrent = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.torrent", + torrentwextra = $"{Configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}_withextras.torrent", + editable = me != null, + userproper = HttpUtility.HtmlEncode(_user.ProperName), + username = HttpUtility.HtmlEncode(user), + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasmovie = true, + moviethumbnail = thumb, + movieurl = movie, + movieproperattr = HttpUtility.HtmlAttributeEncode(_movie.ProperName), + movieproper = HttpUtility.HtmlEncode(_movie.ProperName), + moviename = HttpUtility.HtmlEncode(_movie.Name), + moviedescription = DescriptLinkUtils(_movie.Description ?? "").Replace("\n", "
    ") + }; + } + else + { + value = new + { + username = user, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title, + hasmovie = false + }; + } - await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageMovie.RenderAsync(value))); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await pageMovie.RenderAsync(value))); + } + private async Task ApiPutMovieExtraAsync(ServerContext ctx) + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("movie", out var movie)) + { + + var me = GetAccount(ctx,true); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (me != null) + { + Directory.CreateDirectory(Path.Combine(path, user, "movie", movie)); + var tmpFile = Path.Combine(path, user, "movie", movie, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + await ctx.ReadToFileAsync(tmpFile); + + if (ctx.QueryParams.TryGetFirst("extra", out var extra)) + { + var _path = SanitizePath($"{extra.TrimStart('/')}").TrimStart('/'); + var _path2 = Path.Combine(path, user, "movie", movie, "extras", _path); + if (File.Exists(_path2)) + File.Delete(_path2); + File.Move(tmpFile, _path2); + ScheduleTask(async () => + { + await GenerateBittorentFileMovieAsync(user, movie); + }); + await ctx.SendJsonAsync(new { success = true }); + } + + } + } + } + + private async Task ApiPutShowExtraAsync(ServerContext ctx) + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("show", out var show)) + { + var me = GetAccount(ctx,true); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (me != null) + { + Directory.CreateDirectory(Path.Combine(path, user, "show", show)); + var tmpFile = Path.Combine(path, user, "show", show, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + await ctx.ReadToFileAsync(tmpFile); + + if (ctx.QueryParams.TryGetFirst("extra", out var extra)) + { + var _path = SanitizePath($"{extra.TrimStart('/')}").TrimStart('/'); + var _path2 = Path.Combine(path, user, "show", show, "extras", _path); + if (File.Exists(_path2)) + File.Delete(_path2); + File.Move(tmpFile, _path2); + ScheduleTask(async () => + { + await GenerateBittorentFileShowAsync(user, show); + }); + await ctx.SendJsonAsync(new { success = true }); + } + + } + } + } + private async Task ApiPutAlbumExtraAsync(ServerContext ctx) + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("album", out var album)) + { + var me = GetAccount(ctx,true); + if (me != null && me.Username != user && !me.IsAdmin) + { + me = null; + } + if (me != null) + { + Directory.CreateDirectory(Path.Combine(path, user, "album", album)); + var tmpFile = Path.Combine(path, user, "album", album, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + await ctx.ReadToFileAsync(tmpFile); + + if (ctx.QueryParams.TryGetFirst("extra", out var extra)) + { + var _path = SanitizePath($"{extra.TrimStart('/')}").TrimStart('/'); + var _path2 = Path.Combine(path, user, "album", album, "extras", _path); + if (File.Exists(_path2)) + File.Delete(_path2); + File.Move(tmpFile, _path2); + ScheduleTask(async () => + { + await GenerateBittorentFileAlbumAsync(user, album); + }); + await ctx.SendJsonAsync(new { success = true }); + } + + } + } } private IServer CreateSwagme() { - SwagmeServer swagmeServer=new SwagmeServer(); - swagmeServer.AbsoluteUrl=true; - swagmeServer.Add("/MovieFile",ApiMovieFileAsync,new SwagmeDocumentation("Get the movie resource"),"GET","Movies"); - swagmeServer.Add("/GetMovies",ApiGetMoviesAsync,new SwagmeDocumentation("Get a list of movies","user: the user of the movie
    type: format of list (defaults to json): pls, wiimc, json, m3u8 or rss"),"GET","Movies"); + SwagmeServer swagmeServer = new SwagmeServer(); + swagmeServer.AbsoluteUrl = true; + swagmeServer.Add("/Branding",async(ctx)=>{ + await ctx.SendJsonAsync(new { + title = Configuration.Title + }); + },new SwagmeDocumentation("Branding for server")); + swagmeServer.Add("/Login", ApiLogin, new SwagmeDocumentation("Login to account", "email: the email of account
    password: the password of account
    type: json or cookie"), "POST", "Users"); + swagmeServer.Add("/GetPublicUsers", ApiGetPublicUsers, new SwagmeDocumentation("Get all public users"), "GET", "Users"); + swagmeServer.Add("/Updates", (ctx) => + { + + SendEvents events = new SendEvents(); + void evthdlr(object data) { try { events.SendEvent(data); } catch (Exception ex) { _ = ex; } } + SendEvents += evthdlr; + + ctx.ServerSentEvents(events); + SendEvents -= evthdlr; + + }, new SwagmeDocumentation("Server sent events for updates"), "GET", "Users"); + swagmeServer.Add("/MovieFile", ApiPutMovieFileAsync, new SwagmeDocumentation("Put the movie resource"), "PUT", "Movies"); + swagmeServer.Add("/MovieFile", ApiMovieFileAsync, new SwagmeDocumentation("Get the movie resource"), "GET", "Movies"); + swagmeServer.Add("/MovieExtra", ApiPutMovieExtraAsync, new SwagmeDocumentation("Put the movie extra"), "PUT", "Movies"); + swagmeServer.Add("/GetMovies", ApiGetMoviesAsync, new SwagmeDocumentation("Get a list of movies", "user: the user of the movies
    type: format of list (defaults to json): pls, wiimc, json, m3u8 or rss"), "GET", "Movies"); + + swagmeServer.Add("/ShowExtra", ApiPutShowExtraAsync, new SwagmeDocumentation("Put the show extra"), "PUT", "Shows"); + swagmeServer.Add("/NextEpisode", ApiGetNextEpisodeAsync, new SwagmeDocumentation("Get the next episode if available", "user: the user of the show
    show: the show name
    season: the season number starting from 1
    episode: the episode number starting from 1"), "GET", "Shows"); + swagmeServer.Add("/GetShows", ApiGetShowsAsync, new SwagmeDocumentation("Get a list of shows", "user: the user of the shows
    type: format of list (defaults to json): pls, wiimc, json, m3u8 or rss"), "GET", "Shows"); + swagmeServer.Add("/GetSeasons", ApiGetSeasonsAsync, new SwagmeDocumentation("Get a list of episodes", "user: the user of the show
    show: the show
    type: format of list (defaults to json): pls, wiimc, json, m3u8 or rss"), "GET", "Shows"); + swagmeServer.Add("/GetEpisodes", ApiGetEpisodesAsync, new SwagmeDocumentation("Get a list of episodes", "user: the user of the show
    show: the show
    season: the season
    type: format of list (defaults to json): pls, wiimc, json, m3u8 or rss"), "GET", "Shows"); + swagmeServer.Add("/ShowFile", ApiShowFileAsync, new SwagmeDocumentation("Get the show resource"), "GET", "Shows"); + swagmeServer.Add("/SeasonFile", ApiSeasonFileAsync, new SwagmeDocumentation("Get the season resource"), "GET", "Shows"); + swagmeServer.Add("/EpisodeFile", ApiEpisodeFileAsync, new SwagmeDocumentation("Get the episode resource"), "GET", "Shows"); + + swagmeServer.Add("/AlbumFile", ApiAlbumFileAsync, new SwagmeDocumentation(""), "GET", "Albums"); + swagmeServer.Add("/AlbumFile", ApiPutAlbumFileAsync, new SwagmeDocumentation("Put the album resource"), "PUT", "Albums"); + swagmeServer.Add("/AlbumExtra", ApiPutAlbumExtraAsync, new SwagmeDocumentation("Put the album extra"), "PUT", "Albums"); + swagmeServer.Add("/GetAlbums", ApiGetAlbumsAsync, new SwagmeDocumentation("Get a list of albums", "user: the user of the albums
    type: format of list (defaults to json): json or rss"), "GET", "Albums"); return swagmeServer; } + + private async Task ApiLogin(ServerContext ctx) + { + ctx.ParseBody(); + + if (ctx.QueryParams.TryGetFirst("email", out var email) && ctx.QueryParams.TryGetFirst("password", out var password) && ctx.QueryParams.TryGetFirst("type", out var type) && (type == "json" || type == "cookie")) + { + foreach (var a in provider.GetUsers()) + { + if (a.Email != email) continue; + if (a.Email == email && a.PasswordCorrect(password)) + { + //we got it + byte[] bytes = new byte[32]; + string cookie; + using (var rng = RandomNumberGenerator.Create()) + do + { + + rng.GetBytes(bytes); + cookie = Convert.ToBase64String(bytes); + } while (provider.ContainsSession(cookie)); + + provider.CreateSession(cookie, a.Id); + if (type == "cookie") + { + ctx.ResponseHeaders.Add("Set-Cookie", $"Session={cookie}; Path=/"); + await ctx.SendJsonAsync(new { success = true }); + } + else if (type == "json") + { + await ctx.SendJsonAsync(new { success = true, cookie }); + } + return; + } + + + } + } + await ctx.SendJsonAsync(new { success = false }); + } + + private async Task ApiGetPublicUsers(ServerContext ctx) + { + List users = new List(); + foreach (var user in provider.GetUsers()) + { + if (!user.IsAdmin && Configuration.Publish == CMSPublish.Admin) + { + //await ctx.SendTextAsync(await RenderHtmlAsync(ctx,"

    You can't upload content

    ")); + continue; + } + if (!(user.IsAdmin || user.IsInvited) && Configuration.Publish == CMSPublish.RequireInvite) + { + //await ctx.SendTextAsync(await RenderHtmlAsync(ctx,"

    You can't upload content

    ")); + continue; + } + if (!(user.IsAdmin || user.IsInvited || user.IsVerified)) + { + //await ctx.SendTextAsync(await RenderHtmlAsync(ctx,"

    You can't upload content

    ")); + continue; + } + + users.Add(user); + } + await ctx.SendJsonAsync(users); + } + + private async Task ApiGetShowsAsync(ServerContext ctx) + { + UserAccount a; + if (!ctx.QueryParams.TryGetFirst("type", out var type)) type = "json"; + if (ctx.QueryParams.TryGetFirst("user", out var user)) + { + a = provider.GetUserAccount(user); + } + else + { + a = provider.GetFirstUser(); + user = a.Username; + } + + List shows = new List(); + + if (a != null) + { + shows.AddRange(provider.GetShows(a.Username)); + } + + if (type == "json") + { + await ctx.SendJsonAsync(shows); + } + if (type == "pls" || type == "wiimc") + { + var episodes = GetEpisodesFromShows(shows); + StringBuilder b = new StringBuilder(); + b.AppendLine("[Playlist]"); + int i = 1; + foreach (var item in episodes) + { + var show = provider.GetShow(item.UserId, item.ShowId); + if (type == "wiimc") + b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/content/{HttpUtility.UrlEncode(user)}/show/{show.Name}/Season%20{item.SeasonNumber.ToString("D2")}/S{item.SeasonNumber.ToString("D2")}E{item.EpisodeNumber.ToString("D2")}.mp4"); + else + b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/api/v1/EpisodeFile?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(show.Name)}&season={item.SeasonNumber}&episode={item.EpisodeNumber}&type=download"); + b.AppendLine($"Length{i}=0"); + b.AppendLine($"Title{i}={IniEscape(item.ProperName)}"); + i++; + } + await ctx.SendTextAsync(b.ToString(), "audio/x-scpls"); + } + if (type == "m3u8") + { + var episodes = GetEpisodesFromShows(shows); + M3uPlaylist playlist = new M3uPlaylist(); + foreach (var item in episodes) + { + + M3uPlaylistEntry entry = new M3uPlaylistEntry(); + var show = provider.GetShow(item.UserId, item.ShowId); + entry.Path = $"{Configuration.Root.TrimEnd('/')}/api/v1/EpisodeFile?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(show.Name)}&season={item.SeasonNumber}&episode={item.EpisodeNumber}&type=download"; + entry.Title = item.ProperName; + playlist.PlaylistEntries.Add(entry); + } + M3uContent content = new M3uContent(); + await ctx.SendTextAsync(content.ToText(playlist), "application/x-mpegurl"); + } + if (type == "rss") + { + var episodes = GetEpisodesFromShows(shows); + StringWriter sw = new StringWriter(); + using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true, Indent = false, OmitXmlDeclaration = true, Encoding = Encoding.UTF8 })) + { + + + var rss = new RssFeedWriter(xmlWriter); + + await rss.WriteTitle($"{a.ProperName}'s Movies"); + await rss.WriteGenerator("TessesCMS"); + await rss.WriteValue("link", $"{Configuration.Root.TrimEnd('/')}/"); + foreach (var item in episodes) + { + var show = provider.GetShow(item.UserId, item.ShowId); + AtomEntry entry = new AtomEntry(); + entry.Title = item.ProperName; + entry.Description = $"View here
    {item.Description}"; + entry.LastUpdated = item.LastUpdated; + entry.Published = item.CreationTime; + + await rss.Write(entry); + } + } + await ctx.SendTextAsync(sw.GetStringBuilder().ToString(), "application/rss+xml"); + } + } + public async Task ApiGetSeasonsAsync(ServerContext ctx) + { + UserAccount a; + Show s; + if (!ctx.QueryParams.TryGetFirst("type", out var type)) type = "json"; + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("show", out var show)) + { + a = provider.GetUserAccount(user); + s = provider.GetShow(user, show); + } + else + { + ctx.StatusCode = 404; + await ctx.WriteHeadersAsync(); + return; + } + + + if (type == "json") + { + List seasons = new List(); + for (int i = 1; i < provider.SeasonCount(s.UserId, s.Id); i++) + { + var season = provider.GetSeason(s.UserId, s.Id, i); + if (season != null) + seasons.Add(season); + } + await ctx.SendJsonAsync(seasons); + } + if (type == "pls" || type == "wiimc") + { + var episodes = GetEpisodesFromShow(s); + StringBuilder b = new StringBuilder(); + b.AppendLine("[Playlist]"); + int i = 1; + foreach (var item in episodes) + { + + if (type == "wiimc") + b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/content/{HttpUtility.UrlEncode(user)}/show/{s.Name}/Season%20{item.SeasonNumber.ToString("D2")}/S{item.SeasonNumber.ToString("D2")}E{item.EpisodeNumber.ToString("D2")}.mp4"); + else + b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/api/v1/EpisodeFile?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(s.Name)}&season={item.SeasonNumber}&episode={item.EpisodeNumber}&type=download"); + b.AppendLine($"Length{i}=0"); + b.AppendLine($"Title{i}={IniEscape(item.ProperName)}"); + i++; + } + await ctx.SendTextAsync(b.ToString(), "audio/x-scpls"); + } + if (type == "m3u8") + { + var episodes = GetEpisodesFromShow(s); + M3uPlaylist playlist = new M3uPlaylist(); + foreach (var item in episodes) + { + + M3uPlaylistEntry entry = new M3uPlaylistEntry(); + entry.Path = $"{Configuration.Root.TrimEnd('/')}/api/v1/EpisodeFile?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(s.Name)}&season={item.SeasonNumber}&episode={item.EpisodeNumber}&type=download"; + entry.Title = item.ProperName; + playlist.PlaylistEntries.Add(entry); + } + M3uContent content = new M3uContent(); + await ctx.SendTextAsync(content.ToText(playlist), "application/x-mpegurl"); + } + if (type == "rss") + { + var episodes = GetEpisodesFromShow(s); + StringWriter sw = new StringWriter(); + using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true, Indent = false, OmitXmlDeclaration = true, Encoding = Encoding.UTF8 })) + { + + + var rss = new RssFeedWriter(xmlWriter); + + await rss.WriteTitle($"{a.ProperName}'s Movies"); + await rss.WriteGenerator("TessesCMS"); + await rss.WriteValue("link", $"{Configuration.Root.TrimEnd('/')}/"); + foreach (var item in episodes) + { + AtomEntry entry = new AtomEntry(); + entry.Title = item.ProperName; + entry.Description = $"View here
    {item.Description}"; + entry.LastUpdated = item.LastUpdated; + entry.Published = item.CreationTime; + + await rss.Write(entry); + } + } + await ctx.SendTextAsync(sw.GetStringBuilder().ToString(), "application/rss+xml"); + } + } + public async Task ApiGetEpisodesAsync(ServerContext ctx) + { + UserAccount a; + Show s; + if (!ctx.QueryParams.TryGetFirst("type", out var type)) type = "json"; + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("show", out var show) && ctx.QueryParams.TryGetFirstInt32("season", out var season)) + { + a = provider.GetUserAccount(user); + s = provider.GetShow(user, show); + } + else + { + ctx.StatusCode = 404; + await ctx.WriteHeadersAsync(); + return; + } + + + if (type == "json") + { + List episodes = new List(); + for (int i = 1; i < provider.EpisodeCount(s.UserId, s.Id, season); i++) + { + var episode = provider.GetEpisode(s.UserId, s.Id, season, i); + if (episode != null) + episodes.Add(episode); + } + await ctx.SendJsonAsync(episodes); + } + if (type == "pls" || type == "wiimc") + { + var episodes = GetEpisodesFromSeason(s, season); + StringBuilder b = new StringBuilder(); + b.AppendLine("[Playlist]"); + int i = 1; + foreach (var item in episodes) + { + + if (type == "wiimc") + b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/content/{HttpUtility.UrlEncode(user)}/show/{s.Name}/Season%20{item.SeasonNumber.ToString("D2")}/S{item.SeasonNumber.ToString("D2")}E{item.EpisodeNumber.ToString("D2")}.mp4"); + else + b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/api/v1/EpisodeFile?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(s.Name)}&season={item.SeasonNumber}&episode={item.EpisodeNumber}&type=download"); + b.AppendLine($"Length{i}=0"); + b.AppendLine($"Title{i}={IniEscape(item.ProperName)}"); + i++; + } + await ctx.SendTextAsync(b.ToString(), "audio/x-scpls"); + } + if (type == "m3u8") + { + var episodes = GetEpisodesFromSeason(s, season); + M3uPlaylist playlist = new M3uPlaylist(); + foreach (var item in episodes) + { + + M3uPlaylistEntry entry = new M3uPlaylistEntry(); + entry.Path = $"{Configuration.Root.TrimEnd('/')}/api/v1/EpisodeFile?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(s.Name)}&season={item.SeasonNumber}&episode={item.EpisodeNumber}&type=download"; + entry.Title = item.ProperName; + playlist.PlaylistEntries.Add(entry); + } + M3uContent content = new M3uContent(); + await ctx.SendTextAsync(content.ToText(playlist), "application/x-mpegurl"); + } + if (type == "rss") + { + var episodes = GetEpisodesFromSeason(s, season); + StringWriter sw = new StringWriter(); + using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true, Indent = false, OmitXmlDeclaration = true, Encoding = Encoding.UTF8 })) + { + + + var rss = new RssFeedWriter(xmlWriter); + + await rss.WriteTitle($"{a.ProperName}'s Movies"); + await rss.WriteGenerator("TessesCMS"); + await rss.WriteValue("link", $"{Configuration.Root.TrimEnd('/')}/"); + foreach (var item in episodes) + { + AtomEntry entry = new AtomEntry(); + entry.Title = item.ProperName; + entry.Description = $"View here
    {item.Description}"; + entry.LastUpdated = item.LastUpdated; + entry.Published = item.CreationTime; + + await rss.Write(entry); + } + } + await ctx.SendTextAsync(sw.GetStringBuilder().ToString(), "application/rss+xml"); + } + + } + private async Task ApiGetNextEpisodeAsync(ServerContext ctx) + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("show", out var show) && ctx.QueryParams.TryGetFirst("season", out var seasonS) && ctx.QueryParams.TryGetFirst("episode", out var episodeS)) + { + if (!int.TryParse(seasonS, out var season)) season = 1; + if (!int.TryParse(episodeS, out var episode)) episode = 1; + + var _show = provider.GetShow(user, show); + var _episode = provider.GetEpisode(user, show, season, episode); + + + bool hasnextepisode = false; + + int nextepisodeep = episode + 1; + int nextepisodeseason = season; + while (nextepisodeseason <= provider.SeasonCount(user, show)) + { + + var daseason = provider.GetSeason(user, show, nextepisodeseason); + if (daseason != null) + { + while (nextepisodeep <= provider.EpisodeCount(user, show, nextepisodeseason)) + { + if (File.Exists(Path.Combine(path, user, "show", show, $"Season {nextepisodeseason.ToString("D2")}", $"S{nextepisodeseason.ToString("D2")}E{nextepisodeep.ToString("D2")}.mp4"))) + { + hasnextepisode = true; + break; + } + + nextepisodeep++; + } + } + if (hasnextepisode) + { + break; + } + nextepisodeep = 1; + nextepisodeseason++; + } + + if (hasnextepisode) + { + var _epn = provider.GetEpisode(user, show, nextepisodeseason, nextepisodeep); + string next_episode_page_url = $"{Configuration.Root.TrimEnd('/')}/user/{user}/show/{show}/season/{nextepisodeseason}/episode/{nextepisodeep}/play"; + var data = GetEpisodeContentMetaData(user, show, nextepisodeseason, nextepisodeep); + StringBuilder b = new StringBuilder(); + foreach (var subtitle in data.SubtitlesStreams) + { + string languageName = "Unknown"; + string langCode = ""; + foreach (var lang in Languages) + { + if (lang.LangCode == subtitle.LanguageCode) + { + languageName = lang.LangTitle; + langCode = lang.LangCodeVideo; + } + } + b.Append($""); + + } + await ctx.SendJsonAsync(new { next_subtitles_html = b.ToString(), has_next_episode = true, next_episode_episode = nextepisodeep, next_episode_season = nextepisodeseason, next_episode_name = _epn.EpisodeName, next_episode_url = data.BrowserStream, next_poster_url = data.PosterUrl, next_episode_page_url }); + return; + } + } + await ctx.SendJsonAsync(new { has_next_episode = false }); + } + private async Task ApiPutAlbumFileAsync(ServerContext ctx) + { + try + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("album", out var album)) + { + if (!ctx.QueryParams.TryGetFirst("type", out var type)) + type = "track"; + + if (!ctx.QueryParams.TryGetFirstInt32("track_id", out var track_id)) + track_id = 1; + + var me = GetAccount(ctx,true); + + var _album = provider.GetAlbum(user, album); + + if (me != null && me.Username != user && !me.IsAdmin) + { + + me = null; + } + if (me != null) + { + Directory.CreateDirectory(Path.Combine(path, user, "album", album)); + var tmpFile = Path.Combine(path, user, "album", album, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + await ctx.ReadToFileAsync(tmpFile); + + if (_album != null) + { + + switch (type) + { + case "thumbnail": + string thumb = Path.Combine(path, user, "album", album, "thumbnail.jpg"); + if (File.Exists(thumb)) File.Delete(thumb); + File.Move(tmpFile, thumb); + break; + case "poster": + string poster = Path.Combine(path, user, "album", album, "poster.jpg"); + if (File.Exists(poster)) File.Delete(poster); + File.Move(tmpFile, poster); + break; + case "track": + string oldtrack = _album.Tracks[track_id]; + string flac = Path.Combine(path, user, "album", album, $"{(track_id + 1).ToString("D2")} {_album.AlbumArtist} - {oldtrack}.flac"); + + File.Move(tmpFile, flac); + + ScheduleTask(async () => + { + await GenerateBittorentFileAlbumAsync(user, album); + }); + + string oldmp3 = Path.Combine(path, user, "album", album, $"{oldtrack}.mp3"); + + ScheduleFFmpeg($"-y -i \"{flac}\" {Configuration.BrowserTranscodeMp3} \"{oldmp3}\""); + + break; + } + ScheduleTask(async () => + { + await GenerateBittorentFileAlbumAsync(user, album); + }); + await ctx.SendJsonAsync(new { success = true }); + return; + + + } + } + + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + + private async Task ApiPutMovieFileAsync(ServerContext ctx) + { + try + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("movie", out var movie)) + { + if (!ctx.QueryParams.TryGetFirst("type", out var type)) + type = "movie"; + + var me = GetAccount(ctx,true); + + var _movie = provider.GetMovie(user, movie); + + if (me != null && me.Username != user && !me.IsAdmin) + { + + me = null; + } + if (me != null) + { + Directory.CreateDirectory(Path.Combine(path, user, "movie", movie)); + var tmpFile = Path.Combine(path, user, "movie", movie, $"tmp{DateTime.Now.ToFileTime().ToString()}.bin"); + await ctx.ReadToFileAsync(tmpFile); + + if (_movie != null) + { + + switch (type) + { + case "thumbnail": + string thumb = Path.Combine(path, user, "movie", movie, "thumbnail.jpg"); + if (File.Exists(thumb)) File.Delete(thumb); + File.Move(tmpFile, thumb); + break; + case "poster": + string poster = Path.Combine(path, user, "movie", movie, "poster.jpg"); + if (File.Exists(poster)) File.Delete(poster); + File.Move(tmpFile, poster); + break; + case "movie": + string mov = Path.Combine(path, user, "movie", movie, $"{movie}.mp4"); + if (File.Exists(mov)) File.Delete(mov); + File.Move(tmpFile, mov); + ScheduleFFmpeg($"-y -i \"{mov}\" {Configuration.BrowserTranscode} \"{Path.Combine(path, user, "movie", movie, "browser.mp4")}\""); + break; + } + ScheduleTask(async () => + { + await GenerateBittorentFileMovieAsync(user, movie); + }); + await ctx.SendJsonAsync(new { success = true }); + return; + + + } + } + + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + } + private async Task ApiAlbumFileAsync(ServerContext ctx) + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("album", out var album)) + { + if (!ctx.QueryParams.TryGetFirst("type", out var type)) + type = "json"; + + var info = GetAlbumContentMetadata(user, album); + switch (type) + { + case "thumbnail": + await ctx.SendRedirectAsync(info.ThumbnailUrl); + break; + case "poster": + await ctx.SendRedirectAsync(info.PosterUrl); + break; + case "torrent": + await ctx.SendRedirectAsync(info.AlbumTorrentUrl); + break; + case "torrent_extra": + await ctx.SendRedirectAsync(info.AlbumWithExtrasTorrentUrl); + break; + case "json": + await ctx.SendJsonAsync(info); + break; + case "wiimc": + case "pls": + { + StringBuilder b = new StringBuilder(); + b.AppendLine("[Playlist]"); + int i = 1; + foreach (var item in info.DownloadStreams) + { + + b.AppendLine($"File{i}={IniEscape(item.Url)}"); + b.AppendLine($"Length{i}=0"); + b.AppendLine($"Title{i}={IniEscape(item.Name)}"); + i++; + } + await ctx.SendTextAsync(b.ToString(), "audio/x-scpls"); + } + break; + case "m3u8": + { + M3uPlaylist playlist = new M3uPlaylist(); + foreach (var item in info.DownloadStreams) + { + M3uPlaylistEntry entry = new M3uPlaylistEntry(); + entry.Path = item.Url; + entry.Title = item.Name; + playlist.PlaylistEntries.Add(entry); + } + M3uContent content = new M3uContent(); + await ctx.SendTextAsync(content.ToText(playlist), "application/x-mpegurl"); + } + break; + case "browser": + { + if (ctx.QueryParams.TryGetFirstInt32("track_number", out var track_no)) + { + var res = info.BrowserStreams.Find(e => e.TrackNumber == track_no); + if (res != null) + await ctx.SendRedirectAsync(res.Url); + else + await ctx.SendNotFoundAsync(); + } + else + await ctx.SendNotFoundAsync(); + } + break; + case "download": + { + if (ctx.QueryParams.TryGetFirstInt32("track_number", out var track_no)) + { + var res = info.DownloadStreams.Find(e => e.TrackNumber == track_no); + if (res != null) + await ctx.SendRedirectAsync(res.Url); + else + await ctx.SendNotFoundAsync(); + } + else + await ctx.SendNotFoundAsync(); + } + break; + } + } + } private async Task ApiMovieFileAsync(ServerContext ctx) { - if(ctx.QueryParams.TryGetFirst("user",out var user) && ctx.QueryParams.TryGetFirst("movie", out var movie)) + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("movie", out var movie)) { - if(!ctx.QueryParams.TryGetFirst("type", out var type)) - type="download"; - - var info =GetMovieContentMetaData(Configuration,user,movie); - switch(type) + if (!ctx.QueryParams.TryGetFirst("type", out var type)) + type = "download"; + + var info = GetMovieContentMetaData(user, movie); + switch (type) { case "download": - await ctx.SendRedirectAsync( info.DownloadStream); + await ctx.SendRedirectAsync(info.DownloadStream); break; case "browser": await ctx.SendRedirectAsync(info.BrowserStream); @@ -2053,12 +4872,91 @@ namespace Tesses.CMS } } } + private async Task ApiEpisodeFileAsync(ServerContext ctx) + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("show", out var show) && ctx.QueryParams.TryGetFirstInt32("season", out var season) && ctx.QueryParams.TryGetFirstInt32("episode", out var episode)) + { + if (!ctx.QueryParams.TryGetFirst("type", out var type)) + type = "download"; + + var info = GetEpisodeContentMetaData(user, show, season, episode); + switch (type) + { + case "download": + await ctx.SendRedirectAsync(info.DownloadStream); + break; + case "browser": + await ctx.SendRedirectAsync(info.BrowserStream); + break; + case "thumbnail": + await ctx.SendRedirectAsync(info.ThumbnailUrl); + break; + case "poster": + await ctx.SendRedirectAsync(info.PosterUrl); + break; + case "json": + await ctx.SendJsonAsync(info); + break; + } + } + } + private async Task ApiSeasonFileAsync(ServerContext ctx) + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("show", out var show) && ctx.QueryParams.TryGetFirstInt32("season", out var season)) + { + if (!ctx.QueryParams.TryGetFirst("type", out var type)) + type = "json"; + + var info = GetSeasonContentMetaData(user, show, season); + switch (type) + { + case "thumbnail": + await ctx.SendRedirectAsync(info.ThumbnailUrl); + break; + case "poster": + await ctx.SendRedirectAsync(info.PosterUrl); + break; + case "json": + await ctx.SendJsonAsync(info); + break; + } + } + } + + private async Task ApiShowFileAsync(ServerContext ctx) + { + if (ctx.QueryParams.TryGetFirst("user", out var user) && ctx.QueryParams.TryGetFirst("show", out var show)) + { + if (!ctx.QueryParams.TryGetFirst("type", out var type)) + type = "json"; + + var info = GetShowContentMetaData(user, show); + switch (type) + { + case "thumbnail": + await ctx.SendRedirectAsync(info.ThumbnailUrl); + break; + case "poster": + await ctx.SendRedirectAsync(info.PosterUrl); + break; + case "torrent": + await ctx.SendRedirectAsync(info.ShowTorrentUrl); + break; + case "torrent_extra": + await ctx.SendRedirectAsync(info.ShowWithExtrasTorrentUrl); + break; + case "json": + await ctx.SendJsonAsync(info); + break; + } + } + } private async Task ApiGetMoviesAsync(ServerContext ctx) { UserAccount a; - if(!ctx.QueryParams.TryGetFirst("type",out var type)) type="json"; - if(ctx.QueryParams.TryGetFirst("user",out var user)) + if (!ctx.QueryParams.TryGetFirst("type", out var type)) type = "json"; + if (ctx.QueryParams.TryGetFirst("user", out var user)) { a = provider.GetUserAccount(user); } @@ -2068,25 +4966,25 @@ namespace Tesses.CMS user = a.Username; } - List movies=new List(); + List movies = new List(); - if(a != null) + if (a != null) { movies.AddRange(provider.GetMovies(a.Id)); } - if(type=="json") + if (type == "json") { await ctx.SendJsonAsync(movies); } - if(type == "pls" || type == "wiimc") + if (type == "pls" || type == "wiimc") { - StringBuilder b=new StringBuilder(); + StringBuilder b = new StringBuilder(); b.AppendLine("[Playlist]"); int i = 1; - foreach(var item in movies) + foreach (var item in movies) { - if(type=="wiimc") + if (type == "wiimc") b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/content/{HttpUtility.UrlEncode(user)}/movie/{item.Name}/browser.mp4"); else b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/api/v1/MovieFile?user={HttpUtility.UrlEncode(user)}&movie={HttpUtility.UrlEncode(item.Name)}&type=download"); @@ -2094,141 +4992,321 @@ namespace Tesses.CMS b.AppendLine($"Title{i}={IniEscape(item.ProperName)}"); i++; } - await ctx.SendTextAsync(b.ToString(),"audio/x-scpls"); + await ctx.SendTextAsync(b.ToString(), "audio/x-scpls"); } - if(type == "m3u8") + if (type == "m3u8") { - M3uPlaylist playlist=new M3uPlaylist(); - foreach(var item in movies) + M3uPlaylist playlist = new M3uPlaylist(); + foreach (var item in movies) { - M3uPlaylistEntry entry=new M3uPlaylistEntry(); + M3uPlaylistEntry entry = new M3uPlaylistEntry(); entry.Path = $"{Configuration.Root.TrimEnd('/')}/api/v1/MovieFile?user={HttpUtility.UrlEncode(user)}&movie={HttpUtility.UrlEncode(item.Name)}&type=download"; entry.Title = item.ProperName; playlist.PlaylistEntries.Add(entry); } M3uContent content = new M3uContent(); - await ctx.SendTextAsync(content.ToText(playlist),"application/x-mpegurl"); + await ctx.SendTextAsync(content.ToText(playlist), "application/x-mpegurl"); } - if(type == "rss") + if (type == "rss") { StringWriter sw = new StringWriter(); - using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true, Indent = false,OmitXmlDeclaration=true, Encoding= Encoding.UTF8 })) - { - - + using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true, Indent = false, OmitXmlDeclaration = true, Encoding = Encoding.UTF8 })) + { + + var rss = new RssFeedWriter(xmlWriter); - + await rss.WriteTitle($"{a.ProperName}'s Movies"); await rss.WriteGenerator("TessesCMS"); - await rss.WriteValue("link",$"{Configuration.Root.TrimEnd('/')}/"); - foreach(var item in movies) + await rss.WriteValue("link", $"{Configuration.Root.TrimEnd('/')}/"); + foreach (var item in movies) { - AtomEntry entry=new AtomEntry(); + AtomEntry entry = new AtomEntry(); entry.Title = item.ProperName; entry.Description = $"View here
    {item.Description}"; entry.LastUpdated = item.LastUpdated; entry.Published = item.CreationTime; - + await rss.Write(entry); } } - await ctx.SendTextAsync(sw.GetStringBuilder().ToString(),"application/rss+xml"); - } + await ctx.SendTextAsync(sw.GetStringBuilder().ToString(), "application/rss+xml"); + } } + private async Task ApiGetAlbumsAsync(ServerContext ctx) + { + UserAccount a; + if (!ctx.QueryParams.TryGetFirst("type", out var type)) type = "json"; + if (ctx.QueryParams.TryGetFirst("user", out var user)) + { + a = provider.GetUserAccount(user); + } + else + { + a = provider.GetFirstUser(); + user = a.Username; + } + + List albums = new List(); + + if (a != null) + { + albums.AddRange(provider.GetAlbums(a.Id)); + } + + if (type == "json") + { + await ctx.SendJsonAsync(albums); + } + + if (type == "rss") + { + StringWriter sw = new StringWriter(); + using (XmlWriter xmlWriter = XmlWriter.Create(sw, new XmlWriterSettings() { Async = true, Indent = false, OmitXmlDeclaration = true, Encoding = Encoding.UTF8 })) + { + + + var rss = new RssFeedWriter(xmlWriter); + + await rss.WriteTitle($"{a.ProperName}'s Albums"); + await rss.WriteGenerator("TessesCMS"); + await rss.WriteValue("link", $"{Configuration.Root.TrimEnd('/')}/"); + foreach (var item in albums) + { + AtomEntry entry = new AtomEntry(); + entry.Title = item.ProperName; + entry.Description = $"View here
    {item.Description}"; + entry.LastUpdated = item.LastUpdated; + entry.Published = item.CreationTime; + + await rss.Write(entry); + } + } + await ctx.SendTextAsync(sw.GetStringBuilder().ToString(), "application/rss+xml"); + } + } + private string IniEscape(string properName) { - StringBuilder b=new StringBuilder(); - foreach(var c in properName) + StringBuilder b = new StringBuilder(); + foreach (var c in properName) { - if(c == '\\' || c ==';' || c == '\"' || c == '\'' || c == '#' || c == '=' || c == ':') + if (c == '\\' || c == ';' || c == '\"' || c == '\'' || c == '#' || c == '=' || c == ':') { b.Append($"\\{c}"); } - else if(c == '\0') + else if (c == '\0') { b.Append("\\0"); } - else if(c == '\b') + else if (c == '\b') { b.Append("\\b"); } - else if(c == '\a') + else if (c == '\a') { b.Append("\\a"); } - else if(c == '\t') + else if (c == '\t') { b.Append("\\t"); } - else if(c == '\r') + else if (c == '\r') { b.Append("\\r"); - }else if(c == '\n') + } + else if (c == '\n') { b.Append("\\n"); } - else if(c > sbyte.MaxValue) + else if (c > sbyte.MaxValue) { b.Append($"\\x{((int)c).ToString("X4")}"); } - else { + else + { b.Append(c); } } return b.ToString(); } - private async Task RenderHtmlAsync(bool isHome,string body,params CMSNavUrl[] urls) + private async Task RenderHtmlAsync(ServerContext ctx, string body, bool isHome = false, bool isDevcenter = false, bool isLogin = false, bool isUpload = false) { List _urls = new List(); - - foreach(var item in urls) + var account = GetAccount(ctx); + CMSNavUrl accountLink = account != null ? Configuration.RelativeNavUrl(account.ProperName, "account") : Configuration.RelativeNavUrl("Login", "login"); + accountLink.Active = isLogin; + var dc = Configuration.RelativeNavUrl("Devcenter", "devcenter"); + dc.Active = isDevcenter; + var upload = Configuration.RelativeNavUrl("Upload", "upload"); + upload.Active = isUpload; + _urls.Add(upload); + _urls.Add(dc); + _urls.Add(accountLink); + foreach (var item in Configuration.Urls) { - _urls.Add(new{text=item.Text, url=item.Url,active=item.Active}); + _urls.Add(new { text = item.Text, url = item.Url, active = item.Active }); } - foreach(var item in Configuration.Urls) + return await pageShell.RenderAsync(new { - _urls.Add(new{text=item.Text,url=item.Url,active=item.Active}); - } - return await pageShell.RenderAsync(new{ ishome = isHome, body = body, - urls=_urls, - rooturl=$"{Configuration.Root.TrimEnd('/')}/", - title=Configuration.Title + urls = _urls, + rooturl = $"{Configuration.Root.TrimEnd('/')}/", + title = Configuration.Title }); } private async Task Index(ServerContext ctx) { - var account=GetAccount(ctx); - CMSNavUrl accountLink=account != null ? Configuration.RelativeNavUrl(account.ProperName,"account") : Configuration.RelativeNavUrl("Login","login"); - await ctx.SendTextAsync(await RenderHtmlAsync(true,await RenderIndexAsync(),Configuration.RelativeNavUrl("Devcenter","devcenter"), accountLink)); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await RenderIndexAsync(), true)); } - + private async Task Devcenter(ServerContext ctx) { - var link = Configuration.RelativeNavUrl("Devcenter","devcenter"); - link.Active=true; - await ctx.SendTextAsync(await RenderHtmlAsync(false,await RenderDevcenterAsync(),link)); + await ctx.SendTextAsync(await RenderHtmlAsync(ctx, await RenderDevcenterAsync(), false, true)); } private async Task RenderDevcenterAsync() { - return await pageDevcenter.RenderAsync(new{title=Configuration.Title,rooturl=$"{Configuration.Root.TrimEnd('/')}/"}); + return await pageDevcenter.RenderAsync(new { title = Configuration.Title, rooturl = $"{Configuration.Root.TrimEnd('/')}/" }); } - + private async Task RenderIndexAsync() { - return await pageIndex.RenderAsync(new{title=Configuration.Title,rooturl=$"{Configuration.Root.TrimEnd('/')}/"}); + return await pageIndex.RenderAsync(new { title = Configuration.Title, rooturl = $"{Configuration.Root.TrimEnd('/')}/" }); } - public async Task RenderUpload1Async() + public async Task RenderUpload1Async(string csrf) { - return await pageUpload.RenderAsync(new{title=Configuration.Title,rooturl=$"{Configuration.Root.TrimEnd('/')}/"}); - + return await pageUpload.RenderAsync(new { title = Configuration.Title,csrf, rooturl = $"{Configuration.Root.TrimEnd('/')}/" }); + } - public CMSConfiguration Configuration {get;set;}=new CMSConfiguration(); - public IServer Server => routeServer; + public CMSConfiguration Configuration { get; set; } = new CMSConfiguration(); + public IServer Server => new CSRFCatcherServer(routeServer); + } + + internal class CSRFCatcherServer : IServer + { + IServer inside; + public CSRFCatcherServer(IServer server) + { + this.inside = server; + } + + public async Task BeforeAsync(ServerContext ctx) + { + try + { + return await inside.BeforeAsync(ctx); + } + catch (InvalidCSRFException) + { + await ctx.SendTextAsync("

    CSRF token invalid

    Press back, refresh and try again

    "); + } + return true; + } + public async Task PostAsync(ServerContext ctx) + { + try + { + await inside.PostAsync(ctx); + } + catch (InvalidCSRFException) + { + await ctx.SendTextAsync("

    CSRF token invalid

    Press back, refresh and try again

    "); + } + } + public async Task OptionsAsync(ServerContext ctx) + { + try + { + await inside.OptionsAsync(ctx); + } + catch (InvalidCSRFException) + { + await ctx.SendTextAsync("

    CSRF token invalid

    Press back, refresh and try again

    "); + } + } + public async Task OtherAsync(ServerContext ctx) + { + try + { + await inside.OtherAsync(ctx); + } + catch (InvalidCSRFException) + { + await ctx.SendTextAsync("

    CSRF token invalid

    Press back, refresh and try again

    "); + } + } + public async Task GetAsync(ServerContext ctx) + { + try + { + await inside.GetAsync(ctx); + } + catch (InvalidCSRFException) + { + await ctx.SendTextAsync("

    CSRF token invalid

    Press back, refresh and try again

    "); + } + } + + public void AddCors(ServerContext ctx) + { + + } + } + + [Serializable] + internal class InvalidCSRFException : Exception + { + public InvalidCSRFException() + { + } + + public InvalidCSRFException(string message) : base(message) + { + } + + public InvalidCSRFException(string message, Exception innerException) : base(message, innerException) + { + } + + protected InvalidCSRFException(SerializationInfo info, StreamingContext context) : base(info, context) + { + } + } + + public class EpisodeContentMetaData + { + [JsonProperty("episode_info")] + public Episode Info { get; set; } + [JsonProperty("has_browser_stream")] + public bool HasBrowserStream { get; set; } + [JsonProperty("has_download_stream")] + public bool HasDownloadStream { get; set; } + + [JsonProperty("has_poster")] + public bool HasPoster { get; set; } + [JsonProperty("has_thumbnail")] + public bool HasThumbnail { get; set; } + + [JsonProperty("browser_stream")] + public string BrowserStream { get; set; } + + [JsonProperty("download_stream")] + public string DownloadStream { get; set; } + + [JsonProperty("poster_url")] + public string PosterUrl { get; set; } + + [JsonProperty("thumbnail_url")] + + public string ThumbnailUrl { get; set; } + + [JsonProperty("subtitle_streams")] + public List SubtitlesStreams { get; set; } = new List(); + } internal class EmailCreator @@ -2236,31 +5314,106 @@ namespace Tesses.CMS CMSConfiguration configuration; Template emailTemplate; Template verifyTemplate; + Template emailMovie; + Template emailShow; + Template emailAlbum; public EmailCreator(CMSConfiguration configuration) { this.configuration = configuration; emailTemplate = Template.Parse(AssetProvider.ReadAllText("/EmailHtml.html")); verifyTemplate = Template.Parse(AssetProvider.ReadAllText("/VerifyEmail.html")); + emailMovie = Template.Parse(AssetProvider.ReadAllText("/EmailMovie.html")); + emailShow = Template.Parse(AssetProvider.ReadAllText("/EmailShow.html")); + emailAlbum = Template.Parse(AssetProvider.ReadAllText("/EmailAlbum.html")); } - private async Task SendEmailAsync(string email,string emailHtml,string subject) + private async Task SendEmailAsync(string email, string emailHtml, string subject) { - if(InternetAddress.TryParse(email,out var to) && InternetAddress.TryParse(this.configuration.Email.Email,out var from)) - using(var smtp=new SmtpClient()) - { - await smtp.ConnectAsync(configuration.Email.Host,configuration.Email.Port,configuration.Email.Encryption); - await smtp.AuthenticateAsync(configuration.Email.User,configuration.Email.Pass); - MimeMessage message=new MimeMessage(); - message.From.Add(from); - message.To.Add(to); - message.Body = new TextPart(MimeKit.Text.TextFormat.Html){Text=await emailTemplate.RenderAsync(new{Body=emailHtml,Websitename=HttpUtility.HtmlEncode(configuration.Title),Websiteurl=HttpUtility.HtmlAttributeEncode($"{configuration.Root.TrimEnd('/')}/")})}; - message.Subject = subject; - await smtp.SendAsync(message); - } + if (InternetAddress.TryParse(email, out var to) && InternetAddress.TryParse(this.configuration.Email.Email, out var from)) + using (var smtp = new SmtpClient()) + { + await smtp.ConnectAsync(configuration.Email.Host, configuration.Email.Port, configuration.Email.Encryption); + await smtp.AuthenticateAsync(configuration.Email.User, configuration.Email.Pass); + MimeMessage message = new MimeMessage(); + message.From.Add(from); + message.To.Add(to); + message.Body = new TextPart(MimeKit.Text.TextFormat.Html) { Text = await emailTemplate.RenderAsync(new { Body = emailHtml, Websitename = HttpUtility.HtmlEncode(configuration.Title), Websiteurl = HttpUtility.HtmlAttributeEncode($"{configuration.Root.TrimEnd('/')}/") }) }; + message.Subject = subject; + await smtp.SendAsync(message); + } } - public async Task SendVerificationEmailAsync(UserAccount account,string verificationCode) + public async Task SendVerificationEmailAsync(UserAccount account, string verificationCode) { string verifyLink = $"{configuration.Root.TrimEnd('/')}/verify?token={HttpUtility.UrlEncode(verificationCode)}"; - await SendEmailAsync(account.Email,await verifyTemplate.RenderAsync(new{verifyurllink=HttpUtility.HtmlAttributeEncode(verifyLink),verifyurl=HttpUtility.HtmlEncode(verifyLink),Propername=HttpUtility.HtmlEncode(account.ProperName)}),$"Verify email for {configuration.Title}"); + await SendEmailAsync(account.Email, await verifyTemplate.RenderAsync(new { verifyurllink = HttpUtility.HtmlAttributeEncode(verifyLink), verifyurl = HttpUtility.HtmlEncode(verifyLink), Propername = HttpUtility.HtmlEncode(account.ProperName) }), $"Verify email for {configuration.Title}"); + } + public async Task EmailShowAsync(IContentProvider provider, UserAccount account, Show show, bool update = false, string body = "") + { + foreach (var uAccount in account.AccountsToMail) + { + if (!uAccount.EnableShows) continue; + if (!uAccount.EnableShows && update) continue; + + var user = provider.GetUserById(uAccount.UserId); + if (user != null) + { + bool hasmessage = false; + string message = ""; + if (!string.IsNullOrWhiteSpace(body)) + { + message = CMSServer.DescriptLinkUtils(body).Replace("\n", "
    "); + hasmessage = true; + } + await SendEmailAsync(user.Email, await emailShow.RenderAsync(new { hasmessage, message, propername = HttpUtility.HtmlEncode(user.ProperName), showuserproper = HttpUtility.HtmlEncode(account.ProperName), showproper = HttpUtility.HtmlEncode(show.ProperName), updated = update, showurl = $"{configuration.Root.TrimEnd('/')}/user/{account.Username}/show/{show.Name}/" }), $"{account.ProperName} {(update ? "updated" : "created")} the show {show.ProperName}"); + } + } + } + public async Task EmailMovieAsync(IContentProvider provider, UserAccount account, Movie movie, bool update = false, string body = "") + { + foreach (var uAccount in account.AccountsToMail) + { + if (!uAccount.EnableMovies) continue; + if (!uAccount.EnableUpdates && update) continue; + + var user = provider.GetUserById(uAccount.UserId); + if (user != null) + { + bool hasmessage = false; + string message = ""; + if (!string.IsNullOrWhiteSpace(body)) + { + message = CMSServer.DescriptLinkUtils(body).Replace("\n", "
    "); + hasmessage = true; + } + + + + + await SendEmailAsync(user.Email, await emailMovie.RenderAsync(new { hasmessage, message, propername = HttpUtility.HtmlEncode(user.ProperName), movieuserproper = HttpUtility.HtmlEncode(account.ProperName), movieproper = HttpUtility.HtmlEncode(movie.ProperName), updated = update, movieurl = $"{configuration.Root.TrimEnd('/')}/user/{account.Username}/movie/{movie.Name}/" }), $"{account.ProperName} {(update ? "updated" : "created")} the movie {movie.ProperName}"); + } + } + } + + internal async Task EmailAlbumAsync(IContentProvider provider, UserAccount account, Album album, bool update = false, string body = "") + { + foreach (var uAccount in account.AccountsToMail) + { + if (!uAccount.EnableMovies) continue; + if (!uAccount.EnableUpdates && update) continue; + + var user = provider.GetUserById(uAccount.UserId); + if (user != null) + { + bool hasmessage = false; + string message = ""; + if (!string.IsNullOrWhiteSpace(body)) + { + message = CMSServer.DescriptLinkUtils(body).Replace("\n", "
    "); + hasmessage = true; + } + + await SendEmailAsync(user.Email, await emailAlbum.RenderAsync(new { hasmessage, message, propername = HttpUtility.HtmlEncode(user.ProperName), albumuserproper = HttpUtility.HtmlEncode(account.ProperName), albumproper = HttpUtility.HtmlEncode(album.ProperName), updated = update, albumurl = $"{configuration.Root.TrimEnd('/')}/user/{account.Username}/album/{album.Name}/" }), $"{account.ProperName} {(update ? "updated" : "created")} the album {album.ProperName}"); + } + } } } } diff --git a/Tesses.CMS/Episode.cs b/Tesses.CMS/Episode.cs index a6708db..34fe661 100644 --- a/Tesses.CMS/Episode.cs +++ b/Tesses.CMS/Episode.cs @@ -2,30 +2,30 @@ using System; using System.Collections.Generic; using System.IO; using Newtonsoft.Json; - namespace Tesses.CMS { public class Episode { [JsonIgnore] public long Id {get;set;} - + [JsonProperty("proper_name")] public string ProperName {get;set;}=""; - + [JsonProperty("season_number")] public int SeasonNumber {get;set;} - + [JsonProperty("episode_number")] public int EpisodeNumber {get;set;} - + [JsonProperty("episode_name")] public string EpisodeName {get;set;}=""; [JsonIgnore] public long ShowId {get;set;} [JsonIgnore] public long UserId {get;set;} - + [JsonProperty("creation_time")] public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] public DateTime LastUpdated {get;set;} - + [JsonProperty("description")] public string Description {get;set;}=""; diff --git a/Tesses.CMS/Event.cs b/Tesses.CMS/Event.cs new file mode 100644 index 0000000..b9ba6ae --- /dev/null +++ b/Tesses.CMS/Event.cs @@ -0,0 +1,42 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace Tesses.CMS +{ + [JsonConverter(typeof(StringEnumConverter),typeof(SnakeCaseNamingStrategy))] + public enum EventType + { + MovieCreate, + MovieUpdate, + ShowCreate, + ShowUpdate, + AlbumCreate, + AlbumUpdate + } + public class CMSEvent + { + [JsonProperty("eventtype")] + public EventType Type {get;set;} + + [JsonProperty("username")] + public string Username {get;set;}=""; + [JsonProperty("userpropername")] + public string UserProperName {get;set;}=""; + + + [JsonProperty("name")] + public string Name {get;set;}=""; + + [JsonProperty("propername")] + public string ProperName {get;set;}=""; + + [JsonProperty("description")] + public string Description {get;set;}=""; + + [JsonProperty("body")] + public string Body {get;set;}=""; + } + + +} \ No newline at end of file diff --git a/Tesses.CMS/IContentProvider.cs b/Tesses.CMS/IContentProvider.cs index 2dee927..efa5dbc 100644 --- a/Tesses.CMS/IContentProvider.cs +++ b/Tesses.CMS/IContentProvider.cs @@ -11,14 +11,21 @@ namespace Tesses.CMS IEnumerable GetMovies(string user); IEnumerable GetMovies(long user); + IEnumerable GetAlbums(string user); + IEnumerable GetAlbums(long user); UserAccount GetUserAccount(string user); - - Movie CreateMovie(string user,string movie,string properName,string description); + Movie CreateMovie(string user,string movie,string properName,string description); + + Movie CreateMovie(long user,string movie,string properName,string description); Movie GetMovie(string user,string movie); void UpdateMovie(Movie movie); void CreateUser(CMSConfiguration configuration,string user,string properName,string email,string password); + Album CreateAlbum(string user, string album, string properName,string description); + + Album CreateAlbum(long user, string album, string properName,string description); + void UpdateUser(UserAccount account); UserAccount GetUserById(long account); @@ -37,43 +44,166 @@ namespace Tesses.CMS bool ContainsVerificationCode(string code); IEnumerable GetShows(string user); - + IEnumerable GetShows(long user); void UpdateShow(Show show); void UpdateEpisode(Episode episode); void UpdateSeason(Season season); + Show CreateShow(long user, string show,string properName, string description); Show CreateShow(string user, string show, string properName, string description); Show GetShow(string user, string show); + Show GetShow(long user, long show); + int SeasonCount(long user,long show); int SeasonCount(string user,string show); Season GetSeason(string user,string show,int season); + Season GetSeason(long user, long show, int season); Season CreateSeason(string user,string show,int season,string properName,string description); + Season CreateSeason(long user,long show,int season,string properName,string description); + int EpisodeCount(string user,string show,int season); + int EpisodeCount(long user,long show,int season); Episode GetEpisode(string user,string show,int season,int episode); + Episode GetEpisode(long user,long show,int season,int episode); Episode CreateEpisode(string user,string show,int season,int episode,string episodename,string properName,string description); - + + Episode CreateEpisode(long user,long show,int season,int episode,string episodename,string properName,string description); + + + Album GetAlbum(string user,string album); + Album GetAlbum(long user,string album); + void UpdateAlbum(Album album); + } + public class SeasonContentMetaData + { + [JsonProperty("season_info")] + public Season Info {get;set;} + + [JsonProperty("has_poster")] + public bool HasPoster {get;set;} + [JsonProperty("poster_url")] + public string PosterUrl {get;set;} + + [JsonProperty("has_thumbnail")] + public bool HasThumbnail {get;set;} + + [JsonProperty("thumbnail_url")] + + public string ThumbnailUrl {get;set;} + } + public class ShowContentMetaData + { + [JsonProperty("show_info")] + public Show Info {get;set;} + [JsonProperty("has_show_torrent")] + public bool HasShowTorrent{get;set;} + [JsonProperty("show_torrent_url")] + public string ShowTorrentUrl {get;set;} + [JsonProperty("has_show_with_extras_torrent")] + public bool HasShowWithExtrasTorrent{get;set;} + + [JsonProperty("show_with_extras_torrent_url")] + public string ShowWithExtrasTorrentUrl {get;set;} + + + + [JsonProperty("has_poster")] + public bool HasPoster {get;set;} + [JsonProperty("poster_url")] + public string PosterUrl {get;set;} + + [JsonProperty("has_thumbnail")] + public bool HasThumbnail {get;set;} + + [JsonProperty("thumbnail_url")] + + public string ThumbnailUrl {get;set;} + [JsonProperty("extra_streams")] + public List ExtraStreams {get;set;}=new List(); + } + + public class AlbumContentMetaData + { + [JsonProperty("album_info")] + public Album Info {get;set;} + + [JsonProperty("has_album_torrent")] + public bool HasAlbumTorrent{get;set;} + [JsonProperty("album_torrent_url")] + public string AlbumTorrentUrl {get;set;} + [JsonProperty("has_album_with_extras_torrent")] + public bool HasAlbumWithExtrasTorrent{get;set;} + + [JsonProperty("album_with_extras_torrent_url")] + public string AlbumWithExtrasTorrentUrl {get;set;} + [JsonProperty("has_poster")] + public bool HasPoster {get;set;} + [JsonProperty("poster_url")] + public string PosterUrl {get;set;} + + [JsonProperty("has_thumbnail")] + public bool HasThumbnail {get;set;} + + [JsonProperty("thumbnail_url")] + + public string ThumbnailUrl {get;set;} + [JsonProperty("extra_streams")] + public List ExtraStreams {get;set;}=new List(); + + [JsonProperty("browser_streams")] + public List BrowserStreams {get;set;}=new List(); + + [JsonProperty("download_streams")] + public List DownloadStreams {get;set;}=new List(); + } + + public class Track + { + [JsonProperty("track_number")] + public int TrackNumber {get;set;} + + [JsonProperty("url")] + public string Url {get;set;} + + [JsonProperty("name")] + public string Name {get;set;} } public class MovieContentMetaData { + [JsonProperty("movie_info")] + public Movie Info {get;set;} + [JsonProperty("has_movie_torrent")] + public bool HasMovieTorrent{get;set;} [JsonProperty("movie_torrent_url")] public string MovieTorrentUrl {get;set;} + [JsonProperty("has_movie_with_extras_torrent")] + public bool HasMovieWithExtrasTorrent{get;set;} [JsonProperty("movie_with_extras_torrent_url")] public string MovieWithExtrasTorrentUrl {get;set;} + [JsonProperty("has_browser_stream")] + public bool HasBrowserStream {get;set;} + [JsonProperty("has_download_stream")] + public bool HasDownloadStream {get;set;} + [JsonProperty("browser_stream")] public string BrowserStream {get;set;} [JsonProperty("download_stream")] public string DownloadStream {get;set;} - + [JsonProperty("has_poster")] + public bool HasPoster {get;set;} [JsonProperty("poster_url")] public string PosterUrl {get;set;} + [JsonProperty("has_thumbnail")] + public bool HasThumbnail {get;set;} + [JsonProperty("thumbnail_url")] public string ThumbnailUrl {get;set;} diff --git a/Tesses.CMS/Movie.cs b/Tesses.CMS/Movie.cs index ccef98d..d6dc1af 100644 --- a/Tesses.CMS/Movie.cs +++ b/Tesses.CMS/Movie.cs @@ -8,16 +8,17 @@ namespace Tesses.CMS { [JsonIgnore] public long Id {get;set;} - + [JsonProperty("proper_name")] public string ProperName {get;set;}=""; - + [JsonProperty("name")] public string Name {get;set;}=""; [JsonIgnore] public long UserId {get;set;} - + [JsonProperty("created_time")] public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] public DateTime LastUpdated {get;set;} - + [JsonProperty("description")] public string Description {get;set;}=""; diff --git a/Tesses.CMS/Project.cs b/Tesses.CMS/Project.cs new file mode 100644 index 0000000..6ced4f8 --- /dev/null +++ b/Tesses.CMS/Project.cs @@ -0,0 +1,40 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Tesses.CMS +{ + public class Project + { + [JsonIgnore] + public long Id {get;set;} + + [JsonProperty("proper_name")] + public string ProperName {get;set;}=""; + [JsonProperty("name")] + public string Name {get;set;}=""; + [JsonIgnore] + public long UserId {get;set;} + [JsonProperty("creation_time")] + public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] + public DateTime LastUpdated {get;set;} + [JsonProperty("description")] + public string Description {get;set;}=""; + [JsonProperty("source")] + public string Source {get;set;}=""; + [JsonProperty("license")] + public string License {get;set;}=""; + + public object Scriban(string thumbnail) + { + return new { + Proper = System.Web.HttpUtility.HtmlEncode( ProperName), + Name = System.Web.HttpUtility.HtmlEncode(Name), + Description = System.Web.HttpUtility.HtmlEncode(Description), + Thumbnail = thumbnail + }; + } + + } +} \ No newline at end of file diff --git a/Tesses.CMS/ProjectRelease.cs b/Tesses.CMS/ProjectRelease.cs new file mode 100644 index 0000000..87faa03 --- /dev/null +++ b/Tesses.CMS/ProjectRelease.cs @@ -0,0 +1,141 @@ +using System; +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Tesses.CMS +{ + public class ProjectRelease + { + [JsonIgnore] + public long Id {get;set;} + + [JsonIgnore] + + public long ProjectId {get;set;} + + [JsonIgnore] + + public long UserId {get;set;} + [JsonProperty("tag")] + public string Tag {get;set;} + [JsonProperty("release_number")] + public long ReleaseNumber {get;set;} + [JsonProperty("platforms")] + public List Platforms {get;set;}=new List(); + [JsonProperty("creation_time")] + public DateTime CreationTime {get;set;} + } + public enum CPUArchitecture + { + X86, + X86_64, + Arm, + Aarch64, + Mips, + Mips64, + + RiscV, + RiscV64, + + PowerPC, + PowerPC64, + + PowerPCLE, + PowerPC64LE, + + Sparc, + + Sparc64, + + Itanium, + Other + + } + + public enum ProjectReleaseOSPlatform + { + Windows, + Mac, + Linux, + Amiga, + TempleOS, + Android, + Ios, + N64, + GBA, + Gamecube, + DS, + Wii, + DSI, + ThreeDS, + WiiU, + Switch, + PS2, + DOS, + PS3, + PS4, + PS5, + + XBox, + XBox360, + XBoxOne, + XBoxSeriesX, + TessesPlay, + TessesOS, + TessesPDA, + Other + } + + public enum PlatformVariant + { + Msi, + ExeInstaller, + XCopyable, + PortableAppsCom, + Tarball, + + Deb, + + AppImage, + Flatpak, + + Dmg, + + Wad, + + Pkg, + + Apk, + + Ipa, + + Appx, + + Iso, + + InstallScript, + Nupkg, + Npm, + TPkg, + Other, + + } + + public class ProjectReleasePlatform + { + [JsonProperty("cpu_arch")] + public CPUArchitecture Arch {get;set;} + [JsonProperty("cpu_arch_other")] + public string ArchOther {get;set;}=""; + [JsonProperty("platform")] + public ProjectReleaseOSPlatform Platform {get;set;} + [JsonProperty("platform_other")] + public string PlatformOther {get;set;}=""; + [JsonProperty("platform_variant")] + public PlatformVariant Variant {get;set;} + [JsonProperty("platform_variant_other")] + public string VariantOther {get;set;}=""; + [JsonProperty("filename")] + public string FileName {get;set;}=""; + } +} \ No newline at end of file diff --git a/Tesses.CMS/Season.cs b/Tesses.CMS/Season.cs index ed53579..a8c3d45 100644 --- a/Tesses.CMS/Season.cs +++ b/Tesses.CMS/Season.cs @@ -9,18 +9,19 @@ namespace Tesses.CMS { [JsonIgnore] public long Id {get;set;} - + [JsonProperty("proper_name")] public string ProperName {get;set;}=""; - + [JsonProperty("season_number")] public int SeasonNumber {get;set;} [JsonIgnore] public long ShowId {get;set;} [JsonIgnore] public long UserId {get;set;} - + [JsonProperty("creation_time")] public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] public DateTime LastUpdated {get;set;} - + [JsonProperty("description")] public string Description {get;set;}=""; diff --git a/Tesses.CMS/Show.cs b/Tesses.CMS/Show.cs index 55f7909..b5a8b24 100644 --- a/Tesses.CMS/Show.cs +++ b/Tesses.CMS/Show.cs @@ -8,16 +8,17 @@ namespace Tesses.CMS { [JsonIgnore] public long Id {get;set;} - + [JsonProperty("proper_name")] public string ProperName {get;set;}=""; - + [JsonProperty("name")] public string Name {get;set;}=""; [JsonIgnore] public long UserId {get;set;} - + [JsonProperty("creation_time")] public DateTime CreationTime {get;set;} + [JsonProperty("last_updated_time")] public DateTime LastUpdated {get;set;} - + [JsonProperty("description")] public string Description {get;set;}=""; diff --git a/Tesses.CMS/Tesses.CMS.csproj b/Tesses.CMS/Tesses.CMS.csproj index 2cc17f3..0417637 100644 --- a/Tesses.CMS/Tesses.CMS.csproj +++ b/Tesses.CMS/Tesses.CMS.csproj @@ -14,7 +14,7 @@ - + diff --git a/Tesses.CMS/UserAccount.cs b/Tesses.CMS/UserAccount.cs index 5e2dd62..8f8333b 100644 --- a/Tesses.CMS/UserAccount.cs +++ b/Tesses.CMS/UserAccount.cs @@ -19,7 +19,7 @@ namespace Tesses.CMS byte[] bytes=new byte[32]; using(var rnd = RandomNumberGenerator.Create()) rnd.GetBytes(bytes); - + Salt = Convert.ToBase64String(bytes); } public string GetPasswordHash(string password) { @@ -37,7 +37,7 @@ namespace Tesses.CMS } [JsonIgnore] public long Id {get;set;} - + [JsonProperty("username")] public string Username {get;set;}=""; [JsonIgnore] public string PasswordHash {get;set;}=""; @@ -45,9 +45,9 @@ namespace Tesses.CMS public string Email {get;set;}=""; [JsonIgnore] public string Salt {get;set;}=""; - + [JsonProperty("proper_name")] public string ProperName {get;set;}=""; - + [JsonProperty("about_me")] public string AboutMe {get;set;}=""; [JsonIgnore] @@ -72,30 +72,42 @@ namespace Tesses.CMS public bool EnableUpdates {get;set;} public bool EnableMovies {get;set;} - public bool EnabledAlbums {get;set;} + public bool EnableAlbums {get;set;} - public bool EnabledSingles {get;set;} + public bool EnableSingles {get;set;} - public bool EnabledShows {get;set;} + public bool EnableShows {get;set;} - public bool EnabledSoftware {get;set;} + public bool EnableSoftware {get;set;} - public bool EnabledOther {get;set;} + public bool EnableOther {get;set;} } - + public enum WebHookType + { + Ntfy, + Gotify, + Other + } public class WebHook { - public long UserId {get;set;} + public string WebhookName {get;set;}=""; + public string Username {get;set;}=""; - public string Url {get;set;} + public string Url {get;set;}=""; - public bool EnableMovies {get;set;} + public string Key {get;set;}=""; + + public int Priority {get;set;}=2; + + public WebHookType Type {get;set;}= WebHookType.Other; + + public bool EnabledMovies {get;set;} public bool EnabledAlbums {get;set;} - public bool EnabledSingles {get;set;} + public bool EnabledMusicVideos {get;set;} public bool EnabledShows {get;set;}