590 lines
23 KiB
C#
590 lines
23 KiB
C#
using System;
|
|
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 enum TessesCMSContentType
|
|
{
|
|
Movie,
|
|
|
|
Show,
|
|
|
|
Album,
|
|
|
|
MusicVideo,
|
|
|
|
SoftwareProject,
|
|
|
|
Other
|
|
}
|
|
public class TessesCMSClient : IDisposable
|
|
{
|
|
|
|
private static HttpClient CreateHttpClient()
|
|
{
|
|
HttpClientHandler httpClientHandler=new HttpClientHandler();
|
|
httpClientHandler.AllowAutoRedirect=true;
|
|
httpClientHandler.UseCookies=true;
|
|
HttpClient client=new HttpClient(httpClientHandler);
|
|
return client;
|
|
}
|
|
internal HttpClient client;
|
|
internal string rooturl;
|
|
public HttpClient Client => 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,bool ownsClient) : this("https://tessesstudios.com/",client,ownsClient)
|
|
{
|
|
|
|
}
|
|
|
|
|
|
public TessesCMSClient(string url) : this(url,CreateHttpClient(),true)
|
|
{
|
|
|
|
}
|
|
public TessesCMSClient() : this(CreateHttpClient(),true)
|
|
{
|
|
|
|
}
|
|
public async Task<Branding> GetBrandingAsync()
|
|
{
|
|
return JsonConvert.DeserializeObject<Branding>(await client.GetStringAsync($"{rooturl}/api/v1/Branding"));
|
|
}
|
|
public void StartEvents(Action<CMSEvent> 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<CMSEvent>());
|
|
}
|
|
}).Wait(0);
|
|
}
|
|
|
|
public async Task<bool> CreateAsync(string urlname, string propername, string description, TessesCMSContentType type=TessesCMSContentType.Movie,CancellationToken token=default)
|
|
{
|
|
|
|
|
|
Dictionary<string,string> kvp= new Dictionary<string, string>
|
|
{
|
|
{ "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<bool> UploadFilePutAsync(string url,string file,CancellationToken token=default,IProgress<double> progress=null)
|
|
{
|
|
using(var f = File.OpenRead(file))
|
|
return await UploadFilePutAsync(url,f,token,progress);
|
|
}
|
|
public async Task<bool> UploadFilePutAsync(string url, Stream src,CancellationToken token=default,IProgress<double> 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<double> progress=null)
|
|
{
|
|
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<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 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<double> progress;
|
|
|
|
public ProgressContent(Stream src, IProgress<double> 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;
|
|
internal UserClient(TessesCMSClient client)
|
|
{
|
|
this.client = client;
|
|
}
|
|
|
|
public async Task LogoutAsync()
|
|
{
|
|
(await client.client.GetAsync("/logout")).Dispose();
|
|
}
|
|
|
|
|
|
public async Task<LoginToken> CreateTokenAsync(string email,string password)
|
|
{
|
|
Dictionary<string,string> us=new Dictionary<string, string>();
|
|
us.Add("email",email);
|
|
us.Add("password",password);
|
|
us.Add("type","json");
|
|
using(var res=await client.client.PostAsync($"{client.rooturl}/api/v1/Login", new FormUrlEncodedContent(us)))
|
|
|
|
return JsonConvert.DeserializeObject<LoginToken>( 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<UserAccount> GetUsersAsync()
|
|
{
|
|
foreach(var user in JsonConvert.DeserializeObject<List<UserAccount>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetPublicUsers")))
|
|
{
|
|
yield return user;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<Show> GetShowsAsync(string user)
|
|
{
|
|
foreach(var item in JsonConvert.DeserializeObject<List<Show>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetShows?user={HttpUtility.UrlEncode(user)}")))
|
|
{
|
|
yield return item;
|
|
}
|
|
}
|
|
public async IAsyncEnumerable<Season> GetSeasonsAsync(string user,string show)
|
|
{
|
|
foreach(var item in JsonConvert.DeserializeObject<List<Season>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetSeasons?user={HttpUtility.UrlEncode(user)}&show={HttpUtility.UrlEncode(show)}")))
|
|
{
|
|
yield return item;
|
|
}
|
|
}
|
|
public async IAsyncEnumerable<Episode> GetEpisodesAsync(string user,string show,int season)
|
|
{
|
|
foreach(var item in JsonConvert.DeserializeObject<List<Episode>>(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<SeasonWithEpisodes> GetEpisodesAsync(string user,string show)
|
|
{
|
|
await foreach(var season in GetSeasonsAsync(user,show))
|
|
{
|
|
List<Episode> episodes=new List<Episode>();
|
|
await foreach(var episode in GetEpisodesAsync(user,show,season.SeasonNumber))
|
|
{
|
|
episodes.Add(episode);
|
|
}
|
|
yield return new SeasonWithEpisodes(season,episodes);
|
|
}
|
|
}
|
|
public async IAsyncEnumerable<ShowWithSeasonsAndEpisodes> GetEpisodesAsync(string user)
|
|
{
|
|
await foreach(var show in GetShowsAsync(user))
|
|
{
|
|
List<SeasonWithEpisodes> seasons=new List<SeasonWithEpisodes>();
|
|
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)
|
|
{
|
|
this.client = client;
|
|
}
|
|
public async IAsyncEnumerable<Movie> GetMoviesAsync(string user)
|
|
{
|
|
foreach(var item in JsonConvert.DeserializeObject<List<Movie>>(await client.client.GetStringAsync($"{client.rooturl}/api/v1/GetMovies?user={HttpUtility.UrlEncode(user)}")))
|
|
{
|
|
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 UploadExtrasAsync(string user, string movie, string dir, CancellationToken token=default,IProgress<double> 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<items.Count;i++)
|
|
{
|
|
await UploadExtraAsync(user,movie,items[i].removePath,items[i].localPath,token,new Progress<double>(e=>{
|
|
double j = i + e;
|
|
progress?.Report(j / items.Count);
|
|
}));
|
|
|
|
}
|
|
|
|
}
|
|
public async Task<bool> UploadExtraAsync(string user, string movie,string extra, string src,CancellationToken token=default,IProgress<double> 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<bool> UploadExtraAsync(string user, string movie,string extra, Stream src,CancellationToken token=default,IProgress<double> 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<bool> UploadMovieAsync(string user, string movie, string src,CancellationToken token=default,IProgress<double> 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<bool> UploadMovieAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress<double> 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<bool> UploadPosterAsync(string user, string movie, string src,CancellationToken token=default,IProgress<double> 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<bool> UploadPosterAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress<double> 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<bool> UploadThumbnailAsync(string user, string movie, string src,CancellationToken token=default,IProgress<double> 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<bool> UploadThumbnailAsync(string user, string movie, Stream src,CancellationToken token=default,IProgress<double> 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<bool> 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<string,string> kvp= new Dictionary<string, string>
|
|
{
|
|
{ "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<double> progress=null)
|
|
{
|
|
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<double> progress=null)
|
|
{
|
|
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<double> 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<double> 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<double> 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<double> 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<double> 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<double> 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<double> 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<double> 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<double> 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<double> 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<double> progress=null)
|
|
{
|
|
var r = await GetMovieContentMetadataAsync(user,movie);
|
|
List<(string inDir,string outDir)> items=new List<(string inDir, string outDir)>();
|
|
void DownloadExtraDir(List<ExtraDataStream> 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<items.Count;i++)
|
|
{
|
|
await DownloadExtraAsync(user,movie,items[i].inDir,items[i].outDir,token,new Progress<double>(e=>{
|
|
double j = i + e;
|
|
progress?.Report(j / items.Count);
|
|
}));
|
|
|
|
}
|
|
}
|
|
|
|
|
|
public async Task<bool> CreateAsync(string urlname, string propername, string description)
|
|
{
|
|
return await client.CreateAsync(urlname,propername,description);
|
|
}
|
|
}
|
|
}
|
|
|