935 lines
39 KiB
C#
935 lines
39 KiB
C#
|
using System;
|
|||
|
using System.IO;
|
|||
|
using System.Threading.Tasks;
|
|||
|
using Newtonsoft.Json;
|
|||
|
using Tesses.WebServer;
|
|||
|
using Scriban;
|
|||
|
using System.Collections.Generic;
|
|||
|
using Tesses.WebServer.Swagme;
|
|||
|
using System.Net;
|
|||
|
using System.Web;
|
|||
|
using System.Text;
|
|||
|
using System.Xml;
|
|||
|
using Microsoft.SyndicationFeed.Rss;
|
|||
|
using Microsoft.SyndicationFeed.Atom;
|
|||
|
using Microsoft.SyndicationFeed;
|
|||
|
using PlaylistsNET.Models;
|
|||
|
using PlaylistsNET.Content;
|
|||
|
using System.Security.Cryptography;
|
|||
|
using System.Collections.Concurrent;
|
|||
|
using System.Diagnostics;
|
|||
|
using System.Linq;
|
|||
|
namespace Tesses.CMS
|
|||
|
{
|
|||
|
public class CMSServer
|
|||
|
{
|
|||
|
public Dictionary<string,UserAccount> Sessions {get;set;}=new Dictionary<string, UserAccount>();
|
|||
|
Template pageShell;
|
|||
|
Template pageIndex;
|
|||
|
Template pageDevcenter;
|
|||
|
|
|||
|
Template pageMovie;
|
|||
|
Template pageMovies;
|
|||
|
Template pageWatchMovie;
|
|||
|
|
|||
|
Template pageUpload;
|
|||
|
|
|||
|
Template pageUsers;
|
|||
|
Template pageEditMovieDetails;
|
|||
|
|
|||
|
|
|||
|
RouteServer routeServer;
|
|||
|
|
|||
|
PathValueServer usersPathValueServer;
|
|||
|
|
|||
|
PathValueServer moviePathValueServer;
|
|||
|
MountableServer usersMountableServer;
|
|||
|
|
|||
|
RouteServer movieRouteServer;
|
|||
|
|
|||
|
IContentProvider provider;
|
|||
|
string path;
|
|||
|
public CMSServer(string configpath,IContentProvider provider)
|
|||
|
{
|
|||
|
this.provider=provider;
|
|||
|
usersMountableServer=new MountableServer();
|
|||
|
usersPathValueServer = new PathValueServer(usersMountableServer);
|
|||
|
movieRouteServer=new RouteServer();
|
|||
|
moviePathValueServer = new PathValueServer(movieRouteServer);
|
|||
|
string configJson = Path.Combine(configpath,"config.json");
|
|||
|
if(File.Exists(configJson))
|
|||
|
{
|
|||
|
Configuration = JsonConvert.DeserializeObject<CMSConfiguration>(File.ReadAllText(configJson));
|
|||
|
}
|
|||
|
|
|||
|
if(Configuration.BittorrentTrackers.Count == 0)
|
|||
|
{
|
|||
|
Configuration.BittorrentTrackers.AddRange( new string[]{
|
|||
|
"https://t1.hloli.org:443/announce",
|
|||
|
"http://1337.abcvg.info:80/announce",
|
|||
|
"http://tracker.renfei.net:8080/announce",
|
|||
|
"http://tracker.gbitt.info:80/announce",
|
|||
|
"http://p2p.0g.cx:6969/announce",
|
|||
|
"https://tracker.foreverpirates.co:443/announce"
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
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"));
|
|||
|
pageMovies = Template.Parse(AssetProvider.ReadAllText("/MoviesPage.html"));
|
|||
|
pageUsers = Template.Parse(AssetProvider.ReadAllText("/UsersPage.html"));
|
|||
|
pageWatchMovie = Template.Parse(AssetProvider.ReadAllText("/WatchMovie.html"));
|
|||
|
pageUpload = Template.Parse(AssetProvider.ReadAllText("/Upload.html"));
|
|||
|
pageEditMovieDetails = Template.Parse(AssetProvider.ReadAllText("/EditMovieDetails.html"));
|
|||
|
MountableServer mountableServer = new MountableServer(new AssetProvider());
|
|||
|
path=Path.Combine(configpath,"content");
|
|||
|
|
|||
|
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("/login",LoginPostAsync,"POST");
|
|||
|
routeServer.Add("/signup",SignupAsync);
|
|||
|
routeServer.Add("/signup",SignupPostAsync,"POST");
|
|||
|
RegisterUsersPath();
|
|||
|
Task.Factory.StartNew(async()=>{
|
|||
|
while(Running)
|
|||
|
{
|
|||
|
if(tasks.TryDequeue(out var item))
|
|||
|
{
|
|||
|
await item();
|
|||
|
}
|
|||
|
await Task.Delay(TimeSpan.FromSeconds(0.125));
|
|||
|
}
|
|||
|
}).Wait(0);
|
|||
|
}
|
|||
|
public bool Running =true;
|
|||
|
public async Task LoginAsync(ServerContext ctx)
|
|||
|
{
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,await AssetProvider.ReadAllTextAsync("/LoginPage.html")));
|
|||
|
|
|||
|
}
|
|||
|
public async Task SignupAsync(ServerContext ctx)
|
|||
|
{
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,await AssetProvider.ReadAllTextAsync("/SignupPage.html")));
|
|||
|
|
|||
|
}
|
|||
|
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))
|
|||
|
{
|
|||
|
bool error = false,emailTaken=false, nameTaken=false, properNameTaken=false, passwordDontMatch=false, passwordNotGoodEnough=false;
|
|||
|
foreach(var user in provider.GetUsers())
|
|||
|
{
|
|||
|
if(user.Username == name)
|
|||
|
{
|
|||
|
nameTaken=true;
|
|||
|
error=true;
|
|||
|
}
|
|||
|
if(user.Email == email)
|
|||
|
{
|
|||
|
emailTaken=true;
|
|||
|
error=true;
|
|||
|
}
|
|||
|
if(user.ProperName == proper_name)
|
|||
|
{
|
|||
|
properNameTaken=true;
|
|||
|
error=true;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if(password != confirm_password)
|
|||
|
{
|
|||
|
error=true;
|
|||
|
passwordDontMatch=true;
|
|||
|
}
|
|||
|
if(password.Length < 10)
|
|||
|
{
|
|||
|
error=true;
|
|||
|
passwordNotGoodEnough=true;
|
|||
|
}
|
|||
|
|
|||
|
if(error)
|
|||
|
{
|
|||
|
StringBuilder b=new StringBuilder();
|
|||
|
|
|||
|
if(emailTaken)
|
|||
|
{
|
|||
|
b.AppendLine("<h1>Email is taken</h1>");
|
|||
|
}
|
|||
|
if(nameTaken)
|
|||
|
{
|
|||
|
b.AppendLine("<h1>Name is taken</h1>");
|
|||
|
}
|
|||
|
if(properNameTaken)
|
|||
|
{
|
|||
|
b.AppendLine("<h1>Proper Name is taken");
|
|||
|
}
|
|||
|
if(passwordNotGoodEnough)
|
|||
|
{
|
|||
|
b.AppendLine("<h1>Password not good enough</h1>");
|
|||
|
}
|
|||
|
if(passwordDontMatch)
|
|||
|
{
|
|||
|
b.AppendLine("<h1>Passwords don't match</h1>");
|
|||
|
}
|
|||
|
await ctx.SendTextAsync(b.ToString());
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
provider.CreateUser(Configuration,name,proper_name,email,password);
|
|||
|
await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/");
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
public async Task LoginPostAsync(ServerContext ctx)
|
|||
|
{
|
|||
|
ctx.ParseBody();
|
|||
|
if(ctx.QueryParams.TryGetFirst("email",out var email) && ctx.QueryParams.TryGetFirst("password",out var password))
|
|||
|
{
|
|||
|
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(Sessions.ContainsKey(cookie));
|
|||
|
|
|||
|
Sessions.Add(cookie,a);
|
|||
|
ctx.ResponseHeaders.Add("Set-Cookie",$"Session={cookie}; Path=/");
|
|||
|
await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/");
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}
|
|||
|
ctx.StatusCode=401;
|
|||
|
await ctx.SendTextAsync("<h1>Incorrect</h1><a href=\"./\">Home</a> | <a href=\"./login\">Login</a>");
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
private UserAccount GetAccount(ServerContext ctx)
|
|||
|
{
|
|||
|
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")
|
|||
|
{
|
|||
|
if(Sessions.TryGetValue(co[1],out var account))
|
|||
|
return account;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
private async Task UsersAsync(ServerContext ctx)
|
|||
|
{
|
|||
|
List<object> users=new List<object>();
|
|||
|
foreach(var user in provider.GetUsers())
|
|||
|
{
|
|||
|
users.Add(user.Scriban());
|
|||
|
}
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageUsers.RenderAsync(new {
|
|||
|
Users=users,
|
|||
|
rooturl=$"{Configuration.Root.TrimEnd('/')}/"
|
|||
|
})));
|
|||
|
}
|
|||
|
const string badC="/\\\"&,?|:;*@!# ";
|
|||
|
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();
|
|||
|
}
|
|||
|
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))
|
|||
|
{
|
|||
|
var account=GetAccount(ctx);
|
|||
|
if(account != null)
|
|||
|
{
|
|||
|
if(!account.IsAdmin && Configuration.Publish == CMSPublish.Admin)
|
|||
|
{
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,"<h1>You can't upload content</h1>"));
|
|||
|
return;
|
|||
|
}
|
|||
|
if(!(account.IsAdmin || account.IsInvited) && Configuration.Publish == CMSPublish.RequireInvite)
|
|||
|
{
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,"<h1>You can't upload content</h1>"));
|
|||
|
return;
|
|||
|
}
|
|||
|
if(!(account.IsAdmin || account.IsInvited || account.IsVerified))
|
|||
|
{
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,"<h1>You can't upload content</h1>"));
|
|||
|
return;
|
|||
|
}
|
|||
|
name = FixString(name);
|
|||
|
switch(type)
|
|||
|
{
|
|||
|
case "movie":
|
|||
|
provider.CreateMovie(account.Username,name,proper_name,description);
|
|||
|
await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/user/{account.Username}/movie/{name}/edit");
|
|||
|
break;
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/login");
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await ctx.SendRedirectAsync($"{Configuration.Root.TrimEnd('/')}/");
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public async Task UploadPage1(ServerContext ctx)
|
|||
|
{
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,await RenderUpload1Async(),Configuration.RelativeNavUrl("Devcenter","devcenter")));
|
|||
|
|
|||
|
}
|
|||
|
public IServer Movies()
|
|||
|
{
|
|||
|
RouteServer routeServer=new RouteServer();
|
|||
|
routeServer.Add("/",MoviesAsync);
|
|||
|
return routeServer;
|
|||
|
}
|
|||
|
public async Task MoviesAsync(ServerContext ctx)
|
|||
|
{
|
|||
|
string user=usersPathValueServer.GetValue(ctx);
|
|||
|
List<object> movies=new List<object>();
|
|||
|
foreach(var item in provider.GetMovies(user))
|
|||
|
{
|
|||
|
var data = provider.GetMovieContentMetaData(Configuration,user,item.Name);
|
|||
|
movies.Add(item.Scriban(data.ThumbnailUrl));
|
|||
|
}
|
|||
|
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageMovies.RenderAsync(new{Movies=movies})));
|
|||
|
}
|
|||
|
private void RegisterUsersPath()
|
|||
|
{
|
|||
|
RouteServer routeServer=new RouteServer();
|
|||
|
routeServer.Add("/",UserPageAsync);
|
|||
|
usersMountableServer.Mount("/",routeServer);
|
|||
|
usersMountableServer.Mount("/movies/",Movies());
|
|||
|
usersMountableServer.Mount("/movie/",moviePathValueServer);
|
|||
|
RegisterMoviePath();
|
|||
|
}
|
|||
|
|
|||
|
private async Task UserPageAsync(ServerContext ctx)
|
|||
|
{
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,await AssetProvider.ReadAllTextAsync("/UserPage.html")));
|
|||
|
}
|
|||
|
|
|||
|
ConcurrentQueue<Func<Task>> tasks=new ConcurrentQueue<Func<Task>>();
|
|||
|
|
|||
|
private void RegisterMoviePath()
|
|||
|
{
|
|||
|
movieRouteServer.Add("/",MoviePageAsync);
|
|||
|
movieRouteServer.Add("/play",PlayMoviePageAsync);
|
|||
|
movieRouteServer.Add("/edit",EditPageAsync);
|
|||
|
movieRouteServer.Add("/edit",EditPagePostAsync,"POST");
|
|||
|
movieRouteServer.Add("/upload",UploadStreamAsync,"POST");
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
private class PieceStream : Stream
|
|||
|
{
|
|||
|
public const int PieceLength = 16*1024;
|
|||
|
public int CurpieceOffset {get;private set;} =0;
|
|||
|
|
|||
|
public List<byte[]> Pieces {get;private set;}=new List<byte[]>();
|
|||
|
|
|||
|
public byte[] CalculateCurPiece()
|
|||
|
{
|
|||
|
using(var sha1=SHA1.Create())
|
|||
|
{
|
|||
|
return sha1.ComputeHash(Curpiece,0,CurpieceOffset);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
public byte[] Curpiece = new byte[PieceLength];
|
|||
|
public override bool CanRead => false;
|
|||
|
|
|||
|
public override bool CanSeek => false;
|
|||
|
|
|||
|
public override bool CanWrite => true;
|
|||
|
long len;
|
|||
|
public override long Length => len;
|
|||
|
|
|||
|
public bool HasPiece=>CurpieceOffset>0;
|
|||
|
|
|||
|
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
|
|||
|
|
|||
|
public override void Flush()
|
|||
|
{
|
|||
|
}
|
|||
|
|
|||
|
public override int Read(byte[] buffer, int offset, int count)
|
|||
|
{
|
|||
|
throw new NotImplementedException();
|
|||
|
}
|
|||
|
|
|||
|
public override long Seek(long offset, SeekOrigin origin)
|
|||
|
{
|
|||
|
throw new NotImplementedException();
|
|||
|
}
|
|||
|
|
|||
|
public override void SetLength(long value)
|
|||
|
{
|
|||
|
}
|
|||
|
|
|||
|
public override void Write(byte[] buffer, int offset, int count)
|
|||
|
{
|
|||
|
for(int i = 0;i<count;i++)
|
|||
|
{
|
|||
|
byte curByte = buffer[i+offset];
|
|||
|
if(CurpieceOffset < PieceLength)
|
|||
|
{
|
|||
|
Curpiece[CurpieceOffset++] = curByte;
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
Pieces.Add(this.CalculateCurPiece());
|
|||
|
CurpieceOffset=0;
|
|||
|
Curpiece[CurpieceOffset++] = curByte;
|
|||
|
}
|
|||
|
}
|
|||
|
len+=count;
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
private async Task GenerateBittorentFileMovieAsync(string user,string movie)
|
|||
|
{
|
|||
|
|
|||
|
string movieDir = Path.Combine(this.path,user,"movie",movie);
|
|||
|
Console.WriteLine($"Starting torrent creation: {movieDir}");
|
|||
|
Directory.CreateDirectory(movieDir);
|
|||
|
string extrasDir = Path.Combine(movieDir,"extras");
|
|||
|
string subtitles = Path.Combine(movieDir,"subtitles");
|
|||
|
PieceStream strm = new PieceStream();
|
|||
|
//calculate without extras
|
|||
|
List<string[]> paths = new List<string[]>();
|
|||
|
paths.Add(new string[]{$"{movie}.mp4"});
|
|||
|
paths.Add(new string[]{"thumbnail.jpg"});
|
|||
|
paths.Add(new string[]{"poster.jpg"});
|
|||
|
if(Directory.Exists(subtitles))
|
|||
|
GetExtras(paths,new string[]{"subtitles"},subtitles);
|
|||
|
List<long> lengths = new List<long>();
|
|||
|
foreach(var _path in paths.Select<string[],string>(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)
|
|||
|
{
|
|||
|
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});
|
|||
|
}
|
|||
|
string torrentFile = Path.Combine(movieDir,$"{movie}.torrent");
|
|||
|
Console.WriteLine(torrentFile);
|
|||
|
using(var file=File.Create(torrentFile))
|
|||
|
{
|
|||
|
await torrent.ToTorrentFile().WriteToStreamAsync(file);
|
|||
|
}
|
|||
|
Console.WriteLine($"Created Torrent: {torrentFile}");
|
|||
|
if(Directory.Exists(extrasDir))
|
|||
|
{
|
|||
|
List<string[]> paths2 = new List<string[]>();
|
|||
|
GetExtras(paths2,new string[]{"extras"},extrasDir);
|
|||
|
foreach(var _path in paths2.Select<string[],string>(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<string[]> paths2, string[] torrentPath, string 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);
|
|||
|
}
|
|||
|
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;
|
|||
|
paths2.Add(path);
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
private async Task UploadStreamAsync(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)
|
|||
|
{
|
|||
|
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)))
|
|||
|
item.Value.Dispose();
|
|||
|
|
|||
|
if(_movie != null)
|
|||
|
{
|
|||
|
if(ctx.QueryParams.TryGetFirst("type",out var type))
|
|||
|
{
|
|||
|
switch(type)
|
|||
|
{
|
|||
|
case "thumbnail":
|
|||
|
File.Move(tmpFile,Path.Combine(path,user,"movie",movie,"thumbnail.jpg"));
|
|||
|
break;
|
|||
|
case "poster":
|
|||
|
File.Move(tmpFile,Path.Combine(path,user,"movie",movie,"poster.jpg"));
|
|||
|
break;
|
|||
|
case "movie":
|
|||
|
File.Move(tmpFile,Path.Combine(path,user,"movie",movie,$"{movie}.mp4"));
|
|||
|
ScheduleFFmpeg($"-i \"{Path.Combine(path,user,"movie",movie,$"{movie}.mp4")}\" {Configuration.BrowserTranscode} \"{Path.Combine(path,user,"movie",movie,"browser.mp4")}\"");
|
|||
|
break;
|
|||
|
}
|
|||
|
ScheduleTask(async()=>{
|
|||
|
await GenerateBittorentFileMovieAsync(user,movie);
|
|||
|
});
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,"<h1>Success</h1><a href=\"./edit\"><- Back</a>"));
|
|||
|
return;
|
|||
|
}
|
|||
|
|
|||
|
}
|
|||
|
}
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,"<h1>Failed</h1><a href=\"./edit\"><- Back</a>"));
|
|||
|
|
|||
|
}
|
|||
|
private void ScheduleFFmpeg(string command)
|
|||
|
{
|
|||
|
ScheduleTask(async()=>{
|
|||
|
using(Process process=new Process())
|
|||
|
{
|
|||
|
process.StartInfo.Arguments = command;
|
|||
|
process.StartInfo.FileName = "ffmpeg";
|
|||
|
if(process.Start())
|
|||
|
{
|
|||
|
await Task.Run(process.WaitForExit);
|
|||
|
}
|
|||
|
}
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
public void ScheduleTask(Func<Task> task)
|
|||
|
{
|
|||
|
tasks.Enqueue(task);
|
|||
|
}
|
|||
|
|
|||
|
public async Task EditPagePostAsync(ServerContext ctx)
|
|||
|
{
|
|||
|
ctx.ParseBody();
|
|||
|
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)
|
|||
|
{
|
|||
|
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))
|
|||
|
{
|
|||
|
_movie.ProperName = proper_name;
|
|||
|
_movie.Description = description;
|
|||
|
provider.UpdateMovie(_movie);
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,"<h1>Success</h1><a href=\"./edit\"><- Back</a>"));
|
|||
|
return;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,"<h1>Failed</h1><a href=\"./edit\"><- Back</a>"));
|
|||
|
}
|
|||
|
public async Task EditPageAsync(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)
|
|||
|
{
|
|||
|
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)})));
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,"<h1>You are unauthorized to edit this</h1>"));
|
|||
|
}
|
|||
|
}
|
|||
|
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=provider.GetMovieContentMetaData(Configuration,user,movie);
|
|||
|
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};
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
value = new{ username=user, rooturl=$"{Configuration.Root.TrimEnd('/')}/",
|
|||
|
title=Configuration.Title,hasmovie=false};
|
|||
|
}
|
|||
|
|
|||
|
await ctx.SendTextAsync(await pageWatchMovie.RenderAsync(value));
|
|||
|
|
|||
|
}
|
|||
|
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);
|
|||
|
|
|||
|
var me = GetAccount(ctx);
|
|||
|
|
|||
|
if(me != null && me.Username != user && !me.IsAdmin)
|
|||
|
{
|
|||
|
me=null;
|
|||
|
}
|
|||
|
object value;
|
|||
|
if(_movie != null && _user != null)
|
|||
|
{
|
|||
|
var data=provider.GetMovieContentMetaData(Configuration,user,movie);
|
|||
|
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"));
|
|||
|
|
|||
|
string thumb = data.ThumbnailUrl;
|
|||
|
value = new{ downloadurl=data.DownloadStream,torrentexists=torrent, torrentwextraexists=torrent_wextra, torrent=data.MovieTorrentUrl,torrentwextra=data.MovieWithExtrasTorrentUrl , 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=HttpUtility.HtmlEncode(_movie.Description)};
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
value = new{ username=user, rooturl=$"{Configuration.Root.TrimEnd('/')}/",
|
|||
|
title=Configuration.Title,hasmovie=false};
|
|||
|
}
|
|||
|
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,await pageMovie.RenderAsync(value)));
|
|||
|
}
|
|||
|
|
|||
|
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","<b>user</b>: the user of the movie<br><b>type</b>: format of list (defaults to json): pls, json, m3u8 or rss"),"GET","Movies");
|
|||
|
return swagmeServer;
|
|||
|
}
|
|||
|
|
|||
|
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("type", out var type))
|
|||
|
type="download";
|
|||
|
|
|||
|
var info = provider.GetMovieContentMetaData(Configuration,user,movie);
|
|||
|
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 "torrent":
|
|||
|
await ctx.SendRedirectAsync(info.MovieTorrentUrl);
|
|||
|
break;
|
|||
|
case "torrent_extra":
|
|||
|
await ctx.SendRedirectAsync(info.MovieWithExtrasTorrentUrl);
|
|||
|
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))
|
|||
|
{
|
|||
|
a = provider.GetUserAccount(user);
|
|||
|
}
|
|||
|
else
|
|||
|
{
|
|||
|
a = provider.GetFirstUser();
|
|||
|
}
|
|||
|
|
|||
|
List<Movie> movies=new List<Movie>();
|
|||
|
|
|||
|
if(a != null)
|
|||
|
{
|
|||
|
movies.AddRange(provider.GetMovies(a.Id));
|
|||
|
}
|
|||
|
|
|||
|
if(type=="json")
|
|||
|
{
|
|||
|
await ctx.SendJsonAsync(movies);
|
|||
|
}
|
|||
|
if(type == "pls")
|
|||
|
{
|
|||
|
StringBuilder b=new StringBuilder();
|
|||
|
b.AppendLine("[Playlist]");
|
|||
|
int i = 1;
|
|||
|
foreach(var item in movies)
|
|||
|
{
|
|||
|
b.AppendLine($"File{i}={Configuration.Root.TrimEnd('/')}/api/v1/MovieFile?user={HttpUtility.UrlEncode(user)}&movie={HttpUtility.UrlEncode(item.Name)}&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")
|
|||
|
{
|
|||
|
M3uPlaylist playlist=new M3uPlaylist();
|
|||
|
foreach(var item in movies)
|
|||
|
{
|
|||
|
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");
|
|||
|
}
|
|||
|
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 Movies");
|
|||
|
await rss.WriteGenerator("TessesCMS");
|
|||
|
await rss.WriteValue("link",$"{Configuration.Root.TrimEnd('/')}/");
|
|||
|
foreach(var item in movies)
|
|||
|
{
|
|||
|
AtomEntry entry=new AtomEntry();
|
|||
|
entry.Title = item.ProperName;
|
|||
|
entry.Description = $"View <a href=\"{Configuration.Root.TrimEnd('/')}/user/{user}/movie/{HttpUtility.UrlEncode(item.Name)}/\">here</a><br>{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)
|
|||
|
{
|
|||
|
if(c == '\\' || c ==';' || c == '\"' || c == '\'' || c == '#' || c == '=' || c == ':')
|
|||
|
{
|
|||
|
b.Append($"\\{c}");
|
|||
|
}
|
|||
|
else if(c == '\0')
|
|||
|
{
|
|||
|
b.Append("\\0");
|
|||
|
}
|
|||
|
else if(c == '\b')
|
|||
|
{
|
|||
|
b.Append("\\b");
|
|||
|
}
|
|||
|
else if(c == '\a')
|
|||
|
{
|
|||
|
b.Append("\\a");
|
|||
|
}
|
|||
|
else if(c == '\t')
|
|||
|
{
|
|||
|
b.Append("\\t");
|
|||
|
}
|
|||
|
else if(c == '\r')
|
|||
|
{
|
|||
|
b.Append("\\r");
|
|||
|
}else if(c == '\n')
|
|||
|
{
|
|||
|
b.Append("\\n");
|
|||
|
}
|
|||
|
else if(c > sbyte.MaxValue)
|
|||
|
{
|
|||
|
b.Append($"\\x{((int)c).ToString("X4")}");
|
|||
|
}
|
|||
|
else {
|
|||
|
b.Append(c);
|
|||
|
}
|
|||
|
}
|
|||
|
return b.ToString();
|
|||
|
}
|
|||
|
|
|||
|
private async Task<string> RenderHtmlAsync(bool isHome,string body,params CMSNavUrl[] urls)
|
|||
|
{
|
|||
|
List<object> _urls = new List<object>();
|
|||
|
|
|||
|
foreach(var item in urls)
|
|||
|
{
|
|||
|
_urls.Add(new{text=item.Text, url=item.Url,active=item.Active});
|
|||
|
}
|
|||
|
foreach(var item in Configuration.Urls)
|
|||
|
{
|
|||
|
_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
|
|||
|
});
|
|||
|
}
|
|||
|
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));
|
|||
|
}
|
|||
|
|
|||
|
private async Task Devcenter(ServerContext ctx)
|
|||
|
{
|
|||
|
var link = Configuration.RelativeNavUrl("Devcenter","devcenter");
|
|||
|
link.Active=true;
|
|||
|
await ctx.SendTextAsync(await RenderHtmlAsync(false,await RenderDevcenterAsync(),link));
|
|||
|
}
|
|||
|
private async Task<string> RenderDevcenterAsync()
|
|||
|
{
|
|||
|
return await pageDevcenter.RenderAsync(new{title=Configuration.Title,rooturl=$"{Configuration.Root.TrimEnd('/')}/"});
|
|||
|
|
|||
|
}
|
|||
|
|
|||
|
private async Task<string> RenderIndexAsync()
|
|||
|
{
|
|||
|
return await pageIndex.RenderAsync(new{title=Configuration.Title,rooturl=$"{Configuration.Root.TrimEnd('/')}/"});
|
|||
|
}
|
|||
|
public async Task<string> RenderUpload1Async()
|
|||
|
{
|
|||
|
return await pageUpload.RenderAsync(new{title=Configuration.Title,rooturl=$"{Configuration.Root.TrimEnd('/')}/"});
|
|||
|
|
|||
|
}
|
|||
|
public CMSConfiguration Configuration {get;set;}=new CMSConfiguration();
|
|||
|
public IServer Server => routeServer;
|
|||
|
}
|
|||
|
|
|||
|
|
|||
|
}
|