Shows almost done

This commit is contained in:
Mike Nolan 2024-01-03 20:53:13 -06:00
parent 52a0ba1f04
commit f81fb656e8
42 changed files with 2856 additions and 358 deletions

1
.gitignore vendored
View File

@ -482,3 +482,4 @@ $RECYCLE.BIN/
# Vim temporary swap files
*.swp
help.txt

13
Dockerfile Normal file
View File

@ -0,0 +1,13 @@
FROM mcr.microsoft.com/dotnet/sdk:7.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
RUN apt update && apt install -y ffmpeg
WORKDIR /app
COPY --from=build /app .
EXPOSE 62444
ENTRYPOINT ["dotnet", "Tesses.CMS.Server.dll", "/data"]

View File

@ -1,17 +1,14 @@
using Tesses.CMS.Client;
TessesCMSClient client = new TessesCMSClient("http://192.168.0.155:62444/");
TessesCMSClient client = new TessesCMSClient("http://192.168.0.158:62444/");
await foreach(var item in client.Movies.GetMoviesAsync("Blender"))
await foreach(var item in client.Movies.GetMoviesAsync("tesses"))
{
var res=await client.Movies.GetMovieContentMetadataAsync("tesses",item.Name);
Console.WriteLine(item.ProperName);
Console.WriteLine($"\t{res.PosterUrl}");
}
bool success=await client.Users.LoginAsync("johndoe@example.com","Se7enMovie");
if(success)
{
Console.WriteLine("Logged in successfully");
}
//await client.Movies.CreateAsync("HolyLoop","Holy Loop");

View File

@ -1,8 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using Newtonsoft.Json;
@ -41,6 +43,46 @@ namespace Tesses.CMS.Client
{
}
public async Task DownloadFileAsync(string url,string dest,CancellationToken token=default,IProgress<double> progress=null)
{
using(var f = File.Create(dest))
await DownloadFileAsync(url,f,token,progress);
}
public async Task DownloadFileAsync(string url,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{
long offset=0;
long total=0;
if(dest.CanSeek)
{
offset=dest.Length;
dest.Seek(offset,SeekOrigin.Begin);
}
HttpRequestMessage message=new HttpRequestMessage(HttpMethod.Get,url);
if(offset > 0)
{
message.Headers.Range=new System.Net.Http.Headers.RangeHeaderValue(offset,null);
}
var resp=await client.SendAsync(message);
if(resp.StatusCode == System.Net.HttpStatusCode.RequestedRangeNotSatisfiable) return;
resp.EnsureSuccessStatusCode();
total = resp.StatusCode == System.Net.HttpStatusCode.PartialContent ? resp.Content.Headers.ContentLength.GetValueOrDefault() + offset : resp.Content.Headers.ContentLength.GetValueOrDefault();
int read=0;
byte[] buffer=new byte[1024];
using(var srcStrm = await resp.Content.ReadAsStreamAsync())
do {
if(token.IsCancellationRequested) return;
read = await srcStrm.ReadAsync(buffer,0,buffer.Length,token);
if(token.IsCancellationRequested) return;
await dest.WriteAsync(buffer,0,read,token);
offset += read;
if(total > 0)
progress?.Report((double)offset / (double)read);
} while(read>0);
resp.Dispose();
}
public MovieClient Movies => new MovieClient(this);
@ -81,5 +123,18 @@ namespace Tesses.CMS.Client
yield return item;
}
}
public async Task<MovieContentMetaData> GetMovieContentMetadataAsync(string user,string movie)
{
return JsonConvert.DeserializeObject<MovieContentMetaData>(await client.client.GetStringAsync($"{client.rooturl.TrimEnd('/')}/api/v1/MovieFile?movie={movie}&user={user}&type=json"));
}
public async Task DownloadMovieAsync(string user,string movie,string dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress);
}
public async Task DownloadMovieAsync(string user,string movie,Stream dest,CancellationToken token=default,IProgress<double> progress=null)
{
await client.DownloadFileAsync($"{client.rooturl.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4",dest,token,progress);
}
}
}

View File

@ -1,7 +1,59 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Tesses.CMS.Client
{
public class MovieContentMetaData
{
[JsonProperty("movie_torrent_url")]
public string MovieTorrentUrl {get;set;}
[JsonProperty("movie_with_extras_torrent_url")]
public string MovieWithExtrasTorrentUrl {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<SubtitleStream> SubtitlesStreams {get;set;}=new List<SubtitleStream>();
[JsonProperty("extra_streams")]
public List<ExtraDataStream> ExtraStreams {get;set;}=new List<ExtraDataStream>();
}
public class ExtraDataStream
{
[JsonProperty("is_dir")]
public bool IsDir {get;set;}
[JsonProperty("name")]
public string Name {get;set;}
[JsonProperty("items")]
public List<ExtraDataStream> Items {get;set;}=new List<ExtraDataStream>();
[JsonProperty("url")]
public string Url {get;set;}
}
public class SubtitleStream
{
[JsonProperty("language_code")]
public string LanguageCode {get;set;}="";
[JsonProperty("url")]
public string SubtitleUrl {get;set;}
}
public class Movie
{
public string ProperName {get;set;}

View File

@ -0,0 +1,298 @@
using System;
using System.Collections.Generic;
using Tesses.CMS;
using Dapper;
using System.Data.Common;
using System.Linq;
using Newtonsoft.Json;
namespace Tesses.CMS.Providers
{
public class MysqlDapperContentProvider : IContentProvider
{
DbConnection con;
public MysqlDapperContentProvider(DbConnection connection)
{
this.con = connection;
con.Execute("CREATE TABLE IF NOT EXISTS Users (Id bigint NOT NULL PRIMARY KEY AUTOINCREMENT,AboutMe text,AccountsToMail text,Email text,IsAdmin bool,IsInvited bool,IsVerified bool,PasswordHash text,ProperName text,Salt text,Username text,Webhooks text);");
con.Execute("CREATE TABLE IF NOT EXISTS Sessions (Id bigint NOT NULL PRIMARY KEY AUTOINCREMENT,Session text,Account bigint);");
con.Execute("CREATE TABLE IF NOT EXISTS VerificationCodes (Id bigint NOT NULL PRIMARY KEY AUTOINCREMENT,Session text,Account bigint,Expires bigint);");
con.Execute("CREATE TABLE IF NOT EXISTS Movies (Id bigint NOT NULL PRIMARY KEY AUTOINCREMENT,UserId bigint,CreationTime bigint,LastUpdated bigint,Description text,Name text,ProperName text);");
}
public void ChangeSession(string session, long account)
{
con.Execute("UPDATE Sessions set Account = @account WHERE Session = @session;",new{session,account});
}
public bool ContainsSession(string cookie)
{
return con.QueryFirstOrDefault("SELECT * FROM Sessions WHERE Session = @cookie;",new{cookie})!=null;
}
public bool ContainsVerificationCode(string code)
{
var res=con.QueryFirstOrDefault("SELECT * FROM VerificationCodes WHERE Session = @code;",new{code});
if(res != null)
{
long expires = res.Expires;
if(DateTimeOffset.Now>= DateTimeOffset.FromUnixTimeSeconds(expires))
{
con.Execute("DELETE FROM VerificationCodes WHERE Session = @code;",new{code});
return false;
}
return true;
}
return false;
}
public Movie CreateMovie(string user, string movie, string properName, string description)
{
var userId=GetUserAccount(user).Id;
Movie movie1=new Movie();
movie1.UserId = userId;
movie1.Name = movie;
movie1.ProperName=properName;
movie1.Description=description;
con.Execute("INSERT INTO Movies(movie) values (@movie);",new{movie=new DapperMovie(movie1)});
return GetMovie(user,movie);
}
public void CreateSession(string session, long account)
{
con.Execute("INSERT INTO Sessions(Session,Account) values (@session,@account);",new{session,account});
}
public void CreateUser(CMSConfiguration configuration, string user, string properName, string email, string password)
{
bool first=con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users LIMIT 0, 1;") == null;
UserAccount account=new UserAccount();
account.IsAdmin = first;
account.IsVerified = first;
account.Email =email;
account.ProperName = properName;
account.Username = user;
account.NewSalt();
account.PasswordHash = account.GetPasswordHash(password);
DapperUserAccount account1=new DapperUserAccount(account);
con.Execute("INSERT INTO Users(user) values (@user);",new{user=account1});
}
public void CreateVerificationCode(string code, long account)
{
long expires=DateTimeOffset.Now.AddDays(1).ToUnixTimeSeconds();
con.Execute("INSERT INTO VerificationCodes(Session,Account,Expires) values (@code,@account,@expires);",new{code,account,expires});
}
public void DeleteSession(string session)
{
con.Execute("DELETE FROM Sessions WHERE Session = @session;",new{session});
}
public void DeleteVerificationCode(string code)
{
con.Execute("DELETE FROM VerificationCodes WHERE Session = @code;",new{code});
}
public UserAccount GetFirstUser()
{
return con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users LIMIT 0, 1;")?.Account;
}
public Movie GetMovie(string user, string movie)
{
long? id =GetUserAccount(user)?.Id;
if(!id.HasValue) return null;
return con.QueryFirstOrDefault<DapperMovie>("SELECT * FROM Users WHERE UserId=@id; AND Name=@movie",new{movie,id})?.MovieObject;
}
public IEnumerable<Movie> GetMovies(string user)
{
long? id =GetUserAccount(user)?.Id;
if(!id.HasValue) yield break;
foreach(var item in GetMovies(id.Value))
{
yield return item;
}
}
public IEnumerable<Movie> GetMovies(long user)
{
foreach(var item in con.Query<DapperMovie>("SELECT * FROM Movies;"))
{
yield return item.MovieObject;
}
}
public long? GetSession(string session)
{
return con.QueryFirstOrDefault("SELECT * FROM Sessions WHERE Session = @session;",new{session})?.Id;
}
public UserAccount GetUserAccount(string user)
{
return con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users WHERE Username=@user;",new{user})?.Account;
}
public UserAccount GetUserById(long account)
{
return con.QueryFirstOrDefault<DapperUserAccount>("SELECT * FROM Users WHERE Id=@id;",new{id=account})?.Account;
}
public IEnumerable<UserAccount> GetUsers()
{
foreach(var user in con.Query<DapperUserAccount>("SELECT * FROM Users;"))
{
yield return user.Account;
}
}
public long? GetVerificationAccount(string code)
{
var res=con.QueryFirstOrDefault("SELECT * FROM VerificationCodes WHERE Session = @code;",new{code});
if(res != null)
{
long expires = res.Expires;
if(DateTimeOffset.Now>= DateTimeOffset.FromUnixTimeSeconds(expires))
{
con.Execute("DELETE FROM VerificationCodes WHERE Session = @code;",new{code});
return null;
}
return res.Account;
}
return null;
}
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 UpdateUser(UserAccount account)
{
DapperUserAccount account1=new DapperUserAccount(account);
con.Execute("UPDATE Users set user = @user WHERE Id = @id;",new{user=account1,id=account1.Id});
}
}
internal class DapperMovie
{
public long Id {get;set;}
public long UserId {get;set;}
public long CreationTime {get;set;}
public long LastUpdated {get;set;}
public string Description {get;set;}
public string Name {get;set;}
public string ProperName {get;set;}
public Movie MovieObject {
get{
return new Movie(){
UserId = UserId,
CreationTime=DateTimeOffset.FromUnixTimeSeconds(CreationTime).DateTime,
LastUpdated=DateTimeOffset.FromUnixTimeSeconds(LastUpdated).DateTime,
Description=Description,
Id=Id,
Name=Name,
ProperName=ProperName
};
}
set{
UserId=value.UserId;
CreationTime=new DateTimeOffset(value.CreationTime).ToUnixTimeSeconds();
LastUpdated=new DateTimeOffset(value.LastUpdated).ToUnixTimeSeconds();
Description=value.Description;
Id=value.Id;
Name=value.Name;
ProperName=value.ProperName;
}
}
public DapperMovie(Movie movie1)
{
MovieObject = movie1;
}
public DapperMovie()
{
}
}
internal class DapperUserAccount
{
public DapperUserAccount()
{
}
public DapperUserAccount(UserAccount account)
{
Account =account;
}
public long Id {get;set;}
public string AboutMe {get;set;}
public string AccountsToMail { get; set; }
public string Email { get; set; }
public bool IsAdmin {get;set;}
public bool IsInvited {get;set;}
public bool IsVerified {get;set;}
public string PasswordHash {get;set;}
public string ProperName {get;set;}
public string Salt {get;set;}
public string Username {get;set;}
public string Webhooks {get;set;}
public UserAccount Account {
get {
return new UserAccount(){
Id = Id,
AboutMe = AboutMe,
AccountsToMail=JsonConvert.DeserializeObject<List<MailEntry>>(AccountsToMail),
Email=Email,
IsAdmin=IsAdmin,
IsInvited=IsInvited,
IsVerified=IsVerified,
PasswordHash=PasswordHash,
ProperName=ProperName,
Salt=Salt,
Username=Username,
Webhooks = JsonConvert.DeserializeObject<List<WebHook>>(Webhooks)
};
}
set{
Id = value.Id;
AboutMe = value.AboutMe;
AccountsToMail = JsonConvert.SerializeObject(value.AccountsToMail);
Email=value.Email;
IsAdmin = value.IsAdmin;
IsInvited = value.IsInvited;
IsVerified = value.IsVerified;
PasswordHash = value.PasswordHash;
ProperName = value.ProperName;
Salt = value.Salt;
Username = value.Username;
Webhooks = JsonConvert.SerializeObject(value.Webhooks);
}
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.24" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tesses.CMS\Tesses.CMS.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,283 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LiteDB;
using Tesses.CMS;
namespace Tesses.CMS.Providers {
public class LiteDBContentProvider : IContentProvider
{
string dir;
ILiteDatabase db;
public LiteDBContentProvider(string dir)
{
this.dir = dir;
db=new LiteDatabase(Path.Combine(dir,"tesses-cms.db"));
}
public Movie CreateMovie(string user, string movie, string properName, string description)
{
var userId=GetUserAccount(user).Id;
Movie _movie = new Movie(){UserId = userId,Name = movie,ProperName=properName,Description = description};
_movie.Id=Movies.Insert(_movie);
return _movie;
}
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;
}
private ILiteCollection<UserAccount> UserAccounts => db.GetCollection<UserAccount>("users");
private ILiteCollection<Movie> Movies => db.GetCollection<Movie>("movies");
private ILiteCollection<Show> Shows => db.GetCollection<Show>("shows");
private ILiteCollection<Season> Seasons => db.GetCollection<Season>("seasons");
private ILiteCollection<Episode> Episodes => db.GetCollection<Episode>("episodes");
private ILiteCollection<LiteDbSession> Sessions => db.GetCollection<LiteDbSession>("sessions");
private ILiteCollection<LiteDbVerificationToken> VerificationCodes => db.GetCollection<LiteDbVerificationToken>("verificationcodes");
public UserAccount GetFirstUser()
{
return GetUsers().First();
}
public Movie GetMovie(string user, string movie)
{
var userId=GetUserAccount(user).Id;
return Movies.FindOne(e=>e.Name == movie && e.UserId == userId);
}
public Show GetShow(string user,string show)
{
var userId=GetUserAccount(user).Id;
return Shows.FindOne(e=>e.Name == show && e.UserId == userId);
}
public IEnumerable<Movie> GetMovies(string user)
{
var userId = GetUserAccount(user).Id;
return GetMovies(userId);
}
public IEnumerable<Movie> GetMovies(long user)
{
return Movies.Find(e=>e.UserId==user);
}
public IEnumerable<Show> GetShows(long user)
{
return Shows.Find(e=>e.UserId==user);
}
public IEnumerable<Show> GetShows(string user)
{
var userId = GetUserAccount(user).Id;
return GetShows(userId);
}
public UserAccount GetUserAccount(string user)
{
return UserAccounts.FindOne(e=>e.Username == user);
}
public IEnumerable<UserAccount> GetUsers()
{
return UserAccounts.FindAll();
}
public void CreateUser(CMSConfiguration configuration,string user,string properName,string email,string password)
{
bool isOwner = UserAccounts.Count() == 0;
UserAccount account=new UserAccount();
account.IsAdmin=isOwner;
account.IsVerified = isOwner;
account.Username = user;
account.ProperName= properName;
account.NewSalt();
account.PasswordHash=account.GetPasswordHash(password);
account.Email = email;
UserAccounts.Insert(account);
}
public void UpdateMovie(Movie movie)
{
Movies.Update(movie);
}
public void UpdateShow(Show show)
{
Shows.Update(show);
}
public void UpdateUser(UserAccount account)
{
UserAccounts.Update(account);
}
public UserAccount GetUserById(long account)
{
return UserAccounts.FindById(account);
}
public class LiteDbSession
{
public long Id {get;set;}
public string Session {get;set;}
public long Account {get;set;}
}
public class LiteDbVerificationToken
{
public long Id {get;set;}
public string Code {get;set;}
public long Account {get;set;}
public DateTime Expires {get;set;}
}
public void CreateSession(string session, long account)
{
LiteDbSession _session=new LiteDbSession();
_session.Account = account;
_session.Session = session;
Sessions.Insert(_session);
}
public void ChangeSession(string session, long account)
{
var s=Sessions.FindOne(e=>e.Session == session);
if(s!=null){s.Account = account; Sessions.Update(s);}
}
public void DeleteSession(string session)
{
var s=Sessions.FindOne(e=>e.Session == session);
if(s!=null){ Sessions.Delete(s.Id);}
}
public long? GetSession(string session)
{
var s=Sessions.FindOne(e=>e.Session == session);
return s != null ? s.Account : (long?)null;
}
public bool ContainsSession(string cookie)
{
return Sessions.Exists(e=>e.Session == cookie);
}
public void CreateVerificationCode(string code, long account)
{
LiteDbVerificationToken liteDbVerificationToken=new LiteDbVerificationToken();
liteDbVerificationToken.Account = account;
liteDbVerificationToken.Code = code;
liteDbVerificationToken.Expires=DateTime.Now.AddDays(1);
VerificationCodes.Insert(liteDbVerificationToken);
}
public void DeleteVerificationCode(string code)
{
var c = VerificationCodes.FindOne(e=>e.Code==code);
if(c != null){VerificationCodes.Delete(c.Id);}
}
public long? GetVerificationAccount(string code)
{
var c = VerificationCodes.FindOne(e=>e.Code==code);
if(c != null){
if(DateTime.Now > c.Expires)
VerificationCodes.Delete(c.Id);
else
return c.Account;
}
return null;
}
public bool ContainsVerificationCode(string code)
{
return VerificationCodes.Exists(e=>e.Code==code);
}
public int SeasonCount(string user, string show)
{
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;
}
public Season GetSeason(string user, string show, int season)
{
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);
}
public Season CreateSeason(string user, string show, int season, string properName, string description)
{
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;
}
public int EpisodeCount(string user, string show, int season)
{
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;
}
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);
}
public Episode CreateEpisode(string user, string show, int season, int episode, string episodename, string properName, string description)
{
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;
}
public void UpdateEpisode(Episode episode)
{
Episodes.Update(episode);
}
public void UpdateSeason(Season season)
{
Seasons.Update(season);
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LiteDb" Version="5.0.17" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tesses.CMS\Tesses.CMS.csproj" />
</ItemGroup>
</Project>

View File

@ -1,4 +1,5 @@
using Tesses.WebServer;
using Tesses.CMS.Providers;
using Tesses.CMS;
string path = args.Length > 0 ? args[0] : "data";
CMSServer server=new CMSServer(path,new LiteDBContentProvider(path));

View File

@ -15,6 +15,7 @@
<ItemGroup>
<ProjectReference Include="..\Tesses.CMS\Tesses.CMS.csproj" />
<ProjectReference Include="..\Tesses.CMS.Providers.LiteDb\Tesses.CMS.Providers.LiteDb.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,4 @@
<h1>{{propername}}</h1>
<br>
<h4>About me</h4>
<p>{{aboutme}}</p>

View File

@ -0,0 +1,19 @@
<form action="./account" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="exampleFormControlInput1" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="exampleFormControlInput1" placeholder="Proper Name" name="proper_name" value="{{propername}}">
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">About Me</label>
<textarea name="about_me" class="form-control" id="exampleFormControlTextarea1" rows="5">{{aboutme}}</textarea>
</div>
<input type="submit" value="Change Metadata" class="btn btn-primary">
{{if notverified}}
<a class="btn btn-primary" href="./resend">Resend Verification Link</a>
{{end}}
{{if admin}}
<a class="btn btn-primary" href="./manage">Manage Accounts</a>
{{end}}
<a class="btn btn-danger" href="./logout">Logout</a>
</form>

View File

@ -4,9 +4,9 @@
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
</div>
<div class="form-floating">
<textarea class="form-control" placeholder="Description" name="description" id="floatingTextarea">{{description}}</textarea>
<label for="floatingTextarea">Description</label>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Description</label>
<textarea name="description" class="form-control" id="exampleFormControlTextarea1" rows="5">{{description}}</textarea>
</div>
<input type="submit" value="Change Metadata" class="btn btn-primary">

View File

@ -0,0 +1,55 @@
<h1>Change season metadata</h1>
<form action="./edit" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Description</label>
<textarea name="description" class="form-control" id="exampleFormControlTextarea1" rows="5">{{description}}</textarea>
</div>
<input type="submit" value="Change Metadata" class="btn btn-primary">
</form>
<br>
<h1>Add episode</h1>
<form action="./addepisode" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="name" class="form-label">Episode Name</label>
<input type="text" class="form-control" id="name" name="name" aria-describedby="message">
<div id="message" class="form-text">This is the name that is used in Download Name, no spaces or /\&quot;&amp;,?|:;*@!#</div>
</div>
<div class="mb-3">
<label for="number" class="form-label">Episode Number</label>
<input type="number" min="1" class="form-control" id="number" name="number" value="{{newseasonnumber}}">
</div>
<div class="mb-3">
<label for="proper-name2" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name2" name="proper_name" value="">
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea2" class="form-label">Description</label>
<textarea name="description" class="form-control" id="exampleFormControlTextarea2" rows="5"></textarea>
</div>
<input type="submit" value="Add Episode" class="btn btn-primary">
</form>
<br>
<h1>Upload asset files</h1>
<form action="./upload" method="post" enctype="multipart/form-data">
<h1>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)</h1>
<select class="form-select" name="type" aria-label="Select resource">
<option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the show (also used for when episode does not have one))</option>
<option value="poster">Poster (same resoultion as show (for when episode does not have one), what you see on video player before pressing play)</option>
</select>
<div class="mb-3">
<label for="formFileSm" class="form-label">File to upload</label>
<input class="form-control form-control-sm" id="formFileSm" name="file" type="file">
</div>
<input type="submit" value="Submit" class="btn btn-primary">
</form>
<br>
<a href="./extras/" target="_blank" class="btn btn-primary">View/Edit extras</a>

View File

@ -0,0 +1,49 @@
<h1>Change show metadata</h1>
<form action="./edit" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name" value="{{propername}}">
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Description</label>
<textarea name="description" class="form-control" id="exampleFormControlTextarea1" rows="5">{{description}}</textarea>
</div>
<input type="submit" value="Change Metadata" class="btn btn-primary">
</form>
<br>
<h1>Add season</h1>
<form action="./addseason" method="post" enctype="application/x-www-form-urlencoded">
<div class="mb-3">
<label for="number" class="form-label">Season Number</label>
<input type="number" min="1" class="form-control" id="number" name="number" value="{{newseasonnumber}}">
</div>
<div class="mb-3">
<label for="proper-name2" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name2" name="proper_name" value="">
</div>
<div class="mb-3">
<label for="exampleFormControlTextarea2" class="form-label">Description</label>
<textarea name="description" class="form-control" id="exampleFormControlTextarea2" rows="5"></textarea>
</div>
<input type="submit" value="Add Season" class="btn btn-primary">
</form>
<br>
<h1>Upload asset files</h1>
<form action="./upload" method="post" enctype="multipart/form-data">
<h1>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)</h1>
<select class="form-select" name="type" aria-label="Select resource">
<option value="thumbnail" selected>Thumbnail (should be 120x214, the coverart for the show (also used for when season or episode does not have one))</option>
<option value="poster">Poster (same resoultion as show (for when season or episode does not have one), what you see on video player before pressing play)</option>
</select>
<div class="mb-3">
<label for="formFileSm" class="form-label">File to upload</label>
<input class="form-control form-control-sm" id="formFileSm" name="file" type="file">
</div>
<input type="submit" value="Submit" class="btn btn-primary">
</form>
<br>
<a href="./extras/" target="_blank" class="btn btn-primary">View/Edit extras</a>

View File

@ -0,0 +1,3 @@
<h1 align="center"><a href="{{websiteurl}}">{{websitename}}</a></h1>
<hr>
{{body}}

View File

@ -0,0 +1,29 @@
<h1>Extras path: {{path}}</h1>
{{if editable}}
<h4>Create Directory</h4>
<form method="post" enctype="application/x-www-form-urlencoded" action="./mkdir">
<input type="hidden" name="parent" value="{{parent}}">
<div class="mb-3">
<label for="exampleFormControlInput1" class="form-label">Directory name</label>
<input type="text" name="name" class="form-control" id="exampleFormControlInput1" placeholder="Directory name">
<input type="submit" class="btn btn-primary" value="Create Directory">
</div>
</form>
<h4>Upload File</h4>
<form method="post" enctype="multipart/form-data" action="./upload_extra">
<input type="hidden" name="parent" value="{{parent}}">
<div class="mb-3">
<label for="formFileSm" class="form-label">Browse for file</label>
<input class="form-control form-control-sm" id="formFileSm" name="file" type="file">
</div>
<input type="submit" class="btn btn-primary" value="Upload">
</form>
{{end}}
<ul class="list-group">
{{for extra in extras}}
<li class="list-group-item">
<a href="{{extra.path}}">{{extra.type}} {{extra.name}}</a>
</li>
{{ end }}
</ul>

View File

@ -0,0 +1,37 @@
<ul class="list-group">
{{for user in users}}
<li class="list-group-item">
{{user.propername}} ({{user.name}})
<form action="./manage" method="post" enctype="application/x-www-form-urlencoded">
<input type="hidden" name="name" value="{{user.nameattr}}">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="flexCheckVerified{{user.i}}" name="verified" {{if user.isverified}} checked {{end}}>
<label class="form-check-label" for="flexCheckVerified{{user.i}}">
Is Verified
</label>
</div>
<br>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="flexCheckInvited{{user.i}}" name="invited" {{if user.isinvited}} checked {{end}}>
<label class="form-check-label" for="flexCheckInvited{{user.i}}">
Is Invited
</label>
</div>
<br>
<div class="form-check">
<input class="form-check-input" type="checkbox" id="flexCheckAdmin{{user.i}}" name="admin" {{if user.isadmin}} checked {{end}}>
<label class="form-check-label" for="flexCheckAdmin{{user.i}}">
Is Admin
</label>
</div>
<input type="submit" class="btn btn-primary" value="Save">
<a href="{{user.impersonate}}" class="btn btn-primary">Impersonate</a>
</form>
</li>
{{end}}
</ul>

View File

@ -4,10 +4,18 @@
<h1>{{movieproper}}</h1>
<h2>{{userproper}}</h2>
<hr>
{{if moviebrowserexists}}
<a class="btn btn-primary" href="./play">Watch Online</a>
{{end}}
{{if movieexists}}
<a class="btn btn-primary" href="{{downloadurl}}" download="{{movieproperattr}}.mp4">Download</a>
{{end}}
{{if editable}}
<a class="btn btn-primary" href="./edit">Edit</a>
<a class="btn btn-primary" href="./subtitles">Edit Subtitles</a>
{{end}}
{{if extrasexists}}
<a class="btn btn-primary" href="./extras">Extras</a>
{{end}}
{{if torrentexists}}
<a class="btn btn-primary" href="{{torrent}}">Torrent</a>
@ -24,6 +32,7 @@
Description
</button>
</h2>
<br>
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
<div class="accordion-body">
<p>{{moviedescription}}</p>

View File

@ -4,8 +4,16 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{title}}</title>
<link rel="stylesheet" href="{{rooturl}}video-js.css">
<link rel="stylesheet" href="{{rooturl}}bootstrap.min.css">
<style>
.wrapped-link {
width: 120px;
}
.wrapped-link a {
word-wrap: break-word;
}
</style>
</head>
<body>
<header>
@ -34,6 +42,5 @@
{{body}}
</main>
<script src="{{rooturl}}bootstrap.min.js"></script>
<script src="{{rooturl}}video.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,71 @@
<br>
<div class="container">
<ul class="nav nav-underline" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="about-tab" data-bs-toggle="tab" data-bs-target="#about-tab-pane" type="button" role="tab" aria-controls="home-tab-pane" aria-selected="true">About</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="episode-tab" data-bs-toggle="tab" data-bs-target="#episode-tab-pane" type="button" role="tab" aria-controls="episode-tab-pane" aria-selected="false">Episodes</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="about-tab-pane" role="tabpanel" aria-labelledby="about-tab" tabindex="0">
<br>
<img src="{{seasonthumbnail}}" alt="{{seasonproper}}" width="120" height="214">
<h1>{{seasonproper}}</h1>
<h2>{{showproper}}</h2>
<h2>{{userproper}}</h2>
<hr>
{{if editable}}
<a class="btn btn-primary" href="./edit">Edit</a>
{{end}}
<br>
<div class="accordion" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Description
</button>
</h2>
<br>
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
<div class="accordion-body">
<p>{{seasondescription}}</p>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="episode-tab-pane" role="tabpanel" aria-labelledby="episode-tab" tabindex="0">
<div class="container-fluid">
<!--thanks to https://stackoverflow.com/a/21678797-->
<style>
ul {
text-decoration: none;
}
li {
display: inline-block;
vertical-align: top;
}
</style>
<br>
<ul>
{{for episode in episodes}}
<li>
<img src="{{episode.thumbnail}}" width="120" height="214"><br><div class="wrapped-link"><a href="./episode/{{episode.episode}}/">{{episode.proper}}</a></div>
</li>
{{ end }}
</ul>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,77 @@
<br>
<div class="container">
<ul class="nav nav-underline" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="about-tab" data-bs-toggle="tab" data-bs-target="#about-tab-pane" type="button" role="tab" aria-controls="home-tab-pane" aria-selected="true">About</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="season-tab" data-bs-toggle="tab" data-bs-target="#season-tab-pane" type="button" role="tab" aria-controls="season-tab-pane" aria-selected="false">Seasons</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="about-tab-pane" role="tabpanel" aria-labelledby="about-tab" tabindex="0">
<br>
<img src="{{showthumbnail}}" alt="{{showproper}}" width="120" height="214">
<h1>{{showproper}}</h1>
<h2>{{userproper}}</h2>
<hr>
{{if editable}}
<a class="btn btn-primary" href="./edit">Edit</a>
{{end}}
{{if extrasexists}}
<a class="btn btn-primary" href="./extras">Extras</a>
{{end}}
{{if torrentexists}}
<a class="btn btn-primary" href="{{torrent}}">Torrent</a>
{{end}}
{{if torrentwextraexists}}
<a class="btn btn-primary" href="{{torrentwextra}}">Torrent With Extras</a>
{{end}}
<br>
<div class="accordion" id="accordionExample">
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne" aria-expanded="true" aria-controls="collapseOne">
Description
</button>
</h2>
<br>
<div id="collapseOne" class="accordion-collapse collapse" data-bs-parent="#accordionExample">
<div class="accordion-body">
<p>{{showdescription}}</p>
</div>
</div>
</div>
</div>
</div>
<div class="tab-pane fade" id="season-tab-pane" role="tabpanel" aria-labelledby="season-tab" tabindex="0">
<div class="container-fluid">
<!--thanks to https://stackoverflow.com/a/21678797-->
<style>
ul {
text-decoration: none;
}
li {
display: inline-block;
vertical-align: top;
}
</style>
<br>
<ul>
{{for season in seasons}}
<li>
<img src="{{season.thumbnail}}" width="120" height="214"><br><a href="./season/{{season.number}}/">{{season.proper}}</a>
</li>
{{ end }}
</ul>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,24 @@
<div class="container-fluid">
<!--thanks to https://stackoverflow.com/a/21678797-->
<style>
ul {
text-decoration: none;
}
li {
display: inline-block;
vertical-align: top;
}
</style>
<ul>
{{for show in shows}}
<li>
<img src="{{show.thumbnail}}" width="120" height="214"><br><a href="../show/{{show.name}}/">{{show.proper}}</a>
</li>
{{ end }}
</ul>
</div>

View File

@ -0,0 +1,135 @@
<div class="container text-center">
<div class="row">
<div class="col">
<video onloadedmetadata="set_max()" controls id="video_player" src="{{browserfile}}" width="320" height="240"></video><br>
<h3>Current Subtitle</h3>
<label for="begin" class="form-label">Start: </label><br> <input class="form-control" type="number" name="" id="begin"><button onclick="get_begin()" class="btn btn-secondary">To Player</button><button onclick="set_begin()" class="btn btn-primary">From Player</button><br>
<label for="end" class="form-label">End: </label> <br><input class="form-control" type="number" name="" id="end"><button onclick="get_end()" class="btn btn-secondary">To Player</button><button onclick="set_end()" class="btn btn-primary">From Player</button><br>
<label for="text" class="form-label">Text: </label><br> <textarea class="form-control" name="" id="text"></textarea><br>
<button onclick="add_caption()" class="btn btn-primary">Add</button><br>
<button onclick="save()" class="btn btn-secondary">Save</button>
</div>
<div class="col">
<h3>All Subtitles</h3>
<ul id="captions" class="list-group"></ul>
</div>
</div>
</div>
<script>
captions=[];
const video_player = document.getElementById('video_player');
const begin = document.getElementById('begin');
const end = document.getElementById('end');
const text = document.getElementById('text');
const _captions = document.getElementById('captions');
function set_max()
{
begin.max = video_player.duration;
end.max = video_player.duration;
begin.value = 0;
end.value = 0;
}
function add_caption2(item)
{
const li= document.createElement('li');
li.classList.add('list-group-item');
const p=document.createElement('p');
const read = document.createElement('button');
read.innerText="To Current Subtitle";
read.classList.add('btn');
read.classList.add('btn-primary');
const write = document.createElement('button');
write.classList.add('btn');
write.classList.add('btn-secondary');
write.innerText="From Current Subtitle";
const _delete = document.createElement('button');
_delete.classList.add('btn');
_delete.classList.add('btn-danger');
_delete.onclick = ()=>{
var index=captions.indexof(item);
if(index > -1)
{
captions.splice(index,1);
}
_captions.removeChild(li);
};
_delete.innerText="Delete";
p.innerText=`${item.text.substring(0,40)} (${item.begin}=>${item.end})`;
read.onclick = ()=>{
begin.value = item.begin;
end.value = item.end;
text.value = item.text;
};
write.onclick = ()=>{
item.begin = begin.value;
item.end = end.value;
item.text = text.value;
p.innerText=`${item.text.substring(0,40)} (${item.begin}=>${item.end})`;
};
li.appendChild(p);
li.appendChild(read);
li.appendChild(write);
li.appendChild(_delete);
_captions.appendChild(li);
captions.push(item);
}
function add_caption()
{
add_caption2({
begin: begin.value,
end: end.value,
text: text.value
});
}
function get_end()
{
video_player.currentTime = end.value;
}
function get_begin()
{
video_player.currentTime = begin.value;
}
function set_begin()
{
begin.value = video_player.currentTime;
}
function set_end()
{
end.value = video_player.currentTime;
}
function loaded()
{
{{if hasjson}}
{{json}}.forEach(element => {
add_caption2(element);
});
{{end}}
}
addEventListener("DOMContentLoaded", (event) => {loaded();});
function save()
{
fetch("./subtitles?lang={{lang}}",{method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},body: JSON.stringify(captions)/*https://stackoverflow.com/a/29823632*/}).then(e=>{
if(e.ok)
{
alert("Saved");
}
else
{
alert("Failed");
}
e.close();
});
}
</script>

View File

@ -0,0 +1,11 @@
<div class="container">
<form action="./subtitles" method="get">
<select class="form-select" name="lang" aria-label="Language">
<option selected value="">Select Language</option>
{{for lang in languages}}
<option value="{{lang.code}}">{{lang.name}}</option>
{{end}}
</select>
<input type="submit" class="btn btn-primary" value="Next">
</form>
</div>

View File

@ -10,9 +10,9 @@
<label for="proper-name" class="form-label">Proper Name</label>
<input type="text" class="form-control" id="proper-name" name="proper_name">
</div>
<div class="form-floating">
<textarea class="form-control" placeholder="Description" name="description" id="floatingTextarea"></textarea>
<label for="floatingTextarea">Description</label>
<div class="mb-3">
<label for="exampleFormControlTextarea1" class="form-label">Description</label>
<textarea name="description" class="form-control" id="exampleFormControlTextarea1" rows="5"></textarea>
</div>
<div class="form-check">
<input class="form-check-input" checked type="radio" name="type" value="movie" id="movie">

View File

@ -1,4 +1,4 @@
<div class="container">
<ul class="list-group">
<li class="list-group-item">
<a href="./movies/">Movies</a>
@ -22,4 +22,3 @@
<a href="./about">About</a>
</li>
</ul>
</div>

View File

@ -0,0 +1,2 @@
<h3>Hi {{propername}}</h3>
<p>We need to verify your email<br>to verify your account either copy or click this url: <a href="{{verifyurllink}}">{{verifyurl}}</a></p>

View File

@ -0,0 +1 @@
<h1>Verification link sent to {{email}}</h1>

View File

@ -23,7 +23,15 @@ overflow:hidden;}*/
<body>
<video src="{{moviebrowserurl}}" poster="{{movieposter}}" controls>
<!--langcode=langCode,name=languageName,file=subtitle.VttUrl-->
{{for lang in subtitles}}
<track
label="{{lang.name}}"
kind="subtitles"
srclang="{{lang.langcode}}"
src="{{lang.file}}"
/>
{{end}}
</video>
</body>
</html>

View File

@ -5,7 +5,6 @@ using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;
using LiteDB;
using Newtonsoft.Json;
using Tesses.VirtualFilesystem;

View File

@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Mail;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Tesses.WebServer;
@ -9,6 +11,7 @@ namespace Tesses.CMS
{
public class CMSConfiguration
{
public CMSEmailConfiguration Email {get;set;}
public ushort Port {get;set;}=62444;
@ -30,6 +33,21 @@ namespace Tesses.CMS
}
}
public class CMSEmailConfiguration
{
public string Host {get;set;}="";
public string Email {get;set;}="";
public string User {get;set;}="";
public string Pass {get;set;}="";
public int Port {get;set;}=587;
[JsonConverter(typeof(StringEnumConverter))]
public MailKit.Security.SecureSocketOptions Encryption {get;set;}= MailKit.Security.SecureSocketOptions.StartTls;
}
public class CMSNavUrl
{
public CMSNavUrl() : this("","")

File diff suppressed because it is too large Load Diff

45
Tesses.CMS/Episode.cs Normal file
View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
namespace Tesses.CMS
{
public class Episode
{
[JsonIgnore]
public long Id {get;set;}
public string ProperName {get;set;}="";
public int SeasonNumber {get;set;}
public int EpisodeNumber {get;set;}
public string EpisodeName {get;set;}="";
[JsonIgnore]
public long ShowId {get;set;}
[JsonIgnore]
public long UserId {get;set;}
public DateTime CreationTime {get;set;}
public DateTime LastUpdated {get;set;}
public string Description {get;set;}="";
public object Scriban(CMSConfiguration configuration,string dir,string user,string show)
{
return new {
Proper = System.Web.HttpUtility.HtmlEncode( ProperName),
Season = SeasonNumber,
Episode = EpisodeNumber,
Description = System.Web.HttpUtility.HtmlEncode(Description),
Thumbnail = File.Exists(Path.Combine(dir,user,"show",show,$"Season {SeasonNumber.ToString("D2")}",$"{EpisodeName} S{SeasonNumber.ToString("D2")}E{SeasonNumber.ToString("D2")}-thumbnail.jpg")) ? $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{SeasonNumber.ToString("D2")}/{EpisodeName}%20S{SeasonNumber.ToString("D2")}E{SeasonNumber.ToString("D2")}-thumbnail.jpg" : File.Exists(Path.Combine(dir,user,"show",show,$"Season {SeasonNumber.ToString("D2")}","thumbnail.jpg")) ? $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{SeasonNumber.ToString("D2")}/thumbnail.jpg" : $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"
};
}
}
}

View File

@ -18,8 +18,43 @@ namespace Tesses.CMS
void UpdateMovie(Movie movie);
MovieContentMetaData GetMovieContentMetaData(CMSConfiguration configuration,string user,string movie);
void CreateUser(CMSConfiguration configuration,string user,string properName,string email,string password);
void UpdateUser(UserAccount account);
UserAccount GetUserById(long account);
void CreateSession(string session,long account);
void ChangeSession(string session,long account);
void DeleteSession(string session);
long? GetSession(string session);
bool ContainsSession(string cookie);
void CreateVerificationCode(string code,long account);
void DeleteVerificationCode(string code);
long? GetVerificationAccount(string code);
bool ContainsVerificationCode(string code);
IEnumerable<Show> GetShows(string user);
void UpdateShow(Show show);
void UpdateEpisode(Episode episode);
void UpdateSeason(Season season);
Show CreateShow(string user, string show, string properName, string description);
Show GetShow(string user, string show);
int SeasonCount(string user,string show);
Season GetSeason(string user,string show,int season);
Season CreateSeason(string user,string show,int season,string properName,string description);
int EpisodeCount(string user,string show,int season);
Episode GetEpisode(string user,string show,int season,int episode);
Episode CreateEpisode(string user,string show,int season,int episode,string episodename,string properName,string description);
}
public class MovieContentMetaData
@ -67,8 +102,11 @@ namespace Tesses.CMS
[JsonProperty("language_code")]
public string LanguageCode {get;set;}="";
[JsonProperty("url")]
[JsonProperty("srt_url")]
public string SubtitleUrl {get;set;}
public string SrtUrl {get;set;}="";
[JsonProperty("vtt_url")]
public string VttUrl {get;set;}="";
}
}

View File

@ -1,132 +0,0 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LiteDB;
namespace Tesses.CMS {
public class LiteDBContentProvider : IContentProvider
{
string dir;
ILiteDatabase db;
public LiteDBContentProvider(string dir)
{
this.dir = dir;
db=new LiteDatabase(Path.Combine(dir,"tesses-cms.db"));
}
public Movie CreateMovie(string user, string movie, string properName, string description)
{
var userId=GetUserAccount(user).Id;
Movie _movie = new Movie(){UserId = userId,Name = movie,ProperName=properName,Description = description};
_movie.Id=Movies.Insert(_movie);
return _movie;
}
private ILiteCollection<UserAccount> UserAccounts => db.GetCollection<UserAccount>("users");
private ILiteCollection<Movie> Movies => db.GetCollection<Movie>("movies");
public UserAccount GetFirstUser()
{
return GetUsers().First();
}
public Movie GetMovie(string user, string movie)
{
var userId=GetUserAccount(user).Id;
return Movies.FindOne(e=>e.Name == movie && e.UserId == userId);
}
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(dir,"content", user, "movie",movie,"subtitles");
if(Directory.Exists(subDir))
foreach(var language in Directory.GetDirectories(subDir))
{
string languageCode=Path.GetFileName(language); //en-US for english
_movie.SubtitlesStreams.Add(new SubtitleStream(){LanguageCode=languageCode, SubtitleUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/subtitles/{languageCode}/{movie}.srt"});
}
string extrasPath = Path.Combine(dir,"content", 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<ExtraDataStream> 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);
}
}
public IEnumerable<Movie> GetMovies(string user)
{
var userId = GetUserAccount(user).Id;
return GetMovies(userId);
}
public IEnumerable<Movie> GetMovies(long user)
{
return Movies.Find(e=>e.UserId==user);
}
public UserAccount GetUserAccount(string user)
{
return UserAccounts.FindOne(e=>e.Username == user);
}
public IEnumerable<UserAccount> GetUsers()
{
return UserAccounts.FindAll();
}
public void CreateUser(CMSConfiguration configuration,string user,string properName,string email,string password)
{
bool isOwner = UserAccounts.Count() == 0;
UserAccount account=new UserAccount();
account.IsAdmin=isOwner;
account.IsVerified = isOwner;
account.Username = user;
account.ProperName= properName;
account.NewSalt();
account.PasswordHash=account.GetPasswordHash(password);
account.Email = email;
UserAccounts.Insert(account);
}
public void UpdateMovie(Movie movie)
{
Movies.Update(movie);
}
}
}

View File

@ -1,149 +0,0 @@
using System;
using System.Collections.Generic;
namespace Tesses.CMS
{
public class MockProvider : IContentProvider
{
List<Movie> movies=new List<Movie>();
UserAccount account2=new UserAccount();
public MockProvider()
{
Movie movie=new Movie(){Id=1,ProperName="Big Buck Bunny",Name="BigBuckBunny",UserId=1,CreationTime=new System.DateTime(2018,7,24,14,0,0), Description="Blenders Big Buck Bunny" };
movie.LastUpdated = movie.CreationTime;
movies.Add(movie);
account2.IsAdmin=true;
account2.Email = "johndoe@example.com";
account2.ProperName = "John Doe";
account2.Salt = "Jieaifnaienaifeniaienaif";
account2.PasswordHash = "0v4LGsBWyB6LMEuFDzU+H9Iy4Z5T4nyt96FBJ+YXHz0=";
account2.Username = "johndoe";
account2.Id = 2;
UserAccount account1=new UserAccount();
account1.Email="blender@example.com";
account1.PasswordHash="";
account1.ProperName = "Blender Studios";
account1.Username = "Blender";
account1.Salt ="";
account1.Id = 1;
Users.Add(account1);
Users.Add(account2);
}
List<UserAccount> Users =new List<UserAccount>();
public Movie CreateMovie(string user, string movie,string properName, string description)
{
Movie _movie = new Movie();
_movie.ProperName = properName;
_movie.Name = movie;
_movie.Id = movies.Count + 1;
_movie.UserId = 2;
_movie.Description = description;
_movie.CreationTime = DateTime.Now;
_movie.LastUpdated = _movie.CreationTime;
movies.Add(_movie);
return _movie;
}
public Movie GetMovie(string user, string movie)
{
if(user == "Blender" && movie=="BigBuckBunny")
{
return movies[0];
}
foreach(var item in movies)
{
if(item.Name == movie)
{
if(Users[(int)(item.UserId-1)].Username == user)
{
return item;
}
}
}
return null;
}
public UserAccount GetUserAccount(string user)
{
foreach(var _user in Users)
{
if(_user.Username == user) return _user;
}
return null;
}
public IEnumerable<Movie> GetMovies(string user)
{
foreach(var movie in movies)
{
if(Users[(int)(movie.UserId-1)].Username == user)
{
yield return movie;
}
}
}
public IEnumerable<UserAccount> GetUsers()
{
return Users;
}
public void UpdateMovie(Movie movie)
{
throw new NotImplementedException();
}
public UserAccount GetFirstUser()
{
return Users[0];
}
public IEnumerable<Movie> GetMovies(long user)
{
foreach(var movie in movies)
{
if(movie.UserId == user)
{
yield return movie;
}
}
}
public MovieContentMetaData GetMovieContentMetaData(CMSConfiguration configuration, string user, string movie)
{
MovieContentMetaData metaData=new MovieContentMetaData();
metaData.DownloadStream = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.mp4";
metaData.ThumbnailUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/thumbnail.jpg";;
metaData.PosterUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/poster.jpg";
metaData.BrowserStream = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/browser.mp4";
metaData.MovieTorrentUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}.torrent";
metaData.MovieWithExtrasTorrentUrl = $"{configuration.Root.TrimEnd('/')}/content/{user}/movie/{movie}/{movie}_withextras.torrent";
if(user == "Blender" && movie == "BigBuckBunny")
{
metaData.ThumbnailUrl = "https://imgs.search.brave.com/3euIrrvtlxdwORlondpP-QTiWhC1_ARU5VlvgmkEnao/rs:fit:500:0:0/g:ce/aHR0cHM6Ly9tLm1l/ZGlhLWFtYXpvbi5j/b20vaW1hZ2VzL00v/TVY1Qk5qUmpZalJo/Tm1RdE5XRTBZUzAw/TldJd0xXRmhZalV0/TVRrelpUVXdZVEU0/TVRCaVhrRXlYa0Zx/Y0dkZVFYVnlOakEz/T1RJNU1qQUAuanBn";
metaData.BrowserStream= "https://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4";
metaData.DownloadStream = "https://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4";
metaData.PosterUrl = "https://imgs.search.brave.com/3euIrrvtlxdwORlondpP-QTiWhC1_ARU5VlvgmkEnao/rs:fit:500:0:0/g:ce/aHR0cHM6Ly9tLm1l/ZGlhLWFtYXpvbi5j/b20vaW1hZ2VzL00v/TVY1Qk5qUmpZalJo/Tm1RdE5XRTBZUzAw/TldJd0xXRmhZalV0/TVRrelpUVXdZVEU0/TVRCaVhrRXlYa0Zx/Y0dkZVFYVnlOakEz/T1RJNU1qQUAuanBn";
}
return metaData;
}
public void CreateUser(CMSConfiguration configuration, string user,string properName, string email, string password)
{
throw new NotImplementedException();
}
}
}

39
Tesses.CMS/Season.cs Normal file
View File

@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.IO;
using Newtonsoft.Json;
namespace Tesses.CMS
{
public class Season
{
[JsonIgnore]
public long Id {get;set;}
public string ProperName {get;set;}="";
public int SeasonNumber {get;set;}
[JsonIgnore]
public long ShowId {get;set;}
[JsonIgnore]
public long UserId {get;set;}
public DateTime CreationTime {get;set;}
public DateTime LastUpdated {get;set;}
public string Description {get;set;}="";
public object Scriban(CMSConfiguration configuration,string dir,string user,string show)
{
return new {
Proper = System.Web.HttpUtility.HtmlEncode( ProperName),
Number = SeasonNumber,
Description = System.Web.HttpUtility.HtmlEncode(Description),
Thumbnail = File.Exists(Path.Combine(dir,user,"show",show,$"Season {SeasonNumber.ToString("D2")}","thumbnail.jpg")) ? $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/Season%20{SeasonNumber.ToString("D2")}/thumbnail.jpg" : $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{show}/thumbnail.jpg"
};
}
}
}

36
Tesses.CMS/Show.cs Normal file
View File

@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Tesses.CMS
{
public class Show
{
[JsonIgnore]
public long Id {get;set;}
public string ProperName {get;set;}="";
public string Name {get;set;}="";
[JsonIgnore]
public long UserId {get;set;}
public DateTime CreationTime {get;set;}
public DateTime LastUpdated {get;set;}
public string Description {get;set;}="";
public object Scriban(CMSConfiguration configuration,string user)
{
return new {
Proper = System.Web.HttpUtility.HtmlEncode( ProperName),
Name = System.Web.HttpUtility.HtmlEncode(Name),
Description = System.Web.HttpUtility.HtmlEncode(Description),
Thumbnail = $"{configuration.Root.TrimEnd('/')}/content/{user}/show/{Name}/thumbnail.jpg"
};
}
}
}

View File

@ -10,7 +10,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="LiteDb" Version="5.0.17" />
<PackageReference Include="MailKit" Version="4.3.0" />
<PackageReference Include="Microsoft.SyndicationFeed.ReaderWriter" Version="1.0.2" />
<PackageReference Include="PlaylistsNET" Version="1.4.0" />
<PackageReference Include="Scriban" Version="5.9.0" />

View File

@ -69,6 +69,7 @@ namespace Tesses.CMS
{
public long UserId {get;set;}
public bool EnableUpdates {get;set;}
public bool EnableMovies {get;set;}
public bool EnabledAlbums {get;set;}