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 0000000..41a2a61 Binary files /dev/null and b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/Icon.png differ diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MainActivity.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MainActivity.cs new file mode 100644 index 0000000..f88735e --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia.Android/MainActivity.cs @@ -0,0 +1,41 @@ +using Android.App; +using Android.Content; +using Android.Content.PM; +using Android.Runtime; +using Avalonia; +using Avalonia.Android; +using Avalonia.ReactiveUI; +using Tesses.VirtualFilesystem.Filesystems; + +namespace Tesses.CMS.Avalonia.Android; + +[Activity( + Label = "Tesses CMS", + Theme = "@style/MyTheme.NoActionBar", + Icon = "@drawable/icon", + MainLauncher = true, + ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize | ConfigChanges.UiMode)] +public class MainActivity : AvaloniaMainActivity +{ + 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 0000000..da8d49f Binary files /dev/null and b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/avalonia-logo.ico differ diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/no-media-icon.png b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/no-media-icon.png new file mode 100644 index 0000000..c9147ba Binary files /dev/null and b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Assets/no-media-icon.png differ diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Configuration.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Configuration.cs new file mode 100644 index 0000000..79d94a4 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/Configuration.cs @@ -0,0 +1,12 @@ +public class Configuration +{ + public string ServerUrl {get;set;}="https://tessesstudios.com/"; + + public string LoginToken {get;set;}=""; + + public string DownloadPath {get;set;}=""; + + public bool CacheResources {get;set;}=true; + + public bool Log {get;set;}=true; +} \ No newline at end of file diff --git a/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/IPlatform.cs b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/IPlatform.cs new file mode 100644 index 0000000..fd8c8c5 --- /dev/null +++ b/Tesses.CMS.Avalonia/Tesses.CMS.Avalonia/IPlatform.cs @@ -0,0 +1,70 @@ +using System.Threading.Tasks; +using Avalonia.Controls; +using LibVLCSharp.Shared; +using Tesses.VirtualFilesystem; + +public interface IPlatform +{ + Configuration Configuration {get;} + Task BrowseForDownloadDirectoryAsync(); + + bool PlatformUsesNormalPathsForDownload {get;} + + IVirtualFilesystem? DownloadFilesystem {get;} + + Task WriteConfigurationAsync(); + + Task ReadConfigurationAsync(); + + Control CreatePlayer(); + + void SetMediaPlayer(Control control, MediaPlayer? mediaPlayer); + + MediaPlayer? GetMediaPlayer(Control control); + +} + +public class NullPlatform : IPlatform +{ + public bool PlatformUsesNormalPathsForDownload => 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 0000000..9e070f1 Binary files /dev/null and b/Tesses.CMS/Assets/android-chrome-192x192.png differ diff --git a/Tesses.CMS/Assets/android-chrome-512x512.png b/Tesses.CMS/Assets/android-chrome-512x512.png new file mode 100644 index 0000000..7885dd7 Binary files /dev/null and b/Tesses.CMS/Assets/android-chrome-512x512.png differ diff --git a/Tesses.CMS/Assets/apple-touch-icon.png b/Tesses.CMS/Assets/apple-touch-icon.png new file mode 100644 index 0000000..c219475 Binary files /dev/null and b/Tesses.CMS/Assets/apple-touch-icon.png differ 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 0000000..d517f7b Binary files /dev/null and b/Tesses.CMS/Assets/favicon-16x16.png differ diff --git a/Tesses.CMS/Assets/favicon-32x32.png b/Tesses.CMS/Assets/favicon-32x32.png new file mode 100644 index 0000000..e585bd5 Binary files /dev/null and b/Tesses.CMS/Assets/favicon-32x32.png differ diff --git a/Tesses.CMS/Assets/favicon.ico b/Tesses.CMS/Assets/favicon.ico new file mode 100644 index 0000000..9aad621 Binary files /dev/null and b/Tesses.CMS/Assets/favicon.ico differ 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;}