using System; using YoutubeExplode; using YoutubeExplode.Videos; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using System.Net.Http; using Newtonsoft.Json; using System.IO; using YoutubeExplode.Playlists; using YoutubeExplode.Channels; using YoutubeExplode.Search; using System.Security.Cryptography; using System.Text; namespace Tesses.YouTubeDownloader { public abstract class TYTDBase : ITYTDBase { public virtual async Task ThumbnailExistsAsync(VideoId videoId,string res) { return await FileExistsAsync($"Thumbnails/{videoId.Value}/{res}.jpg"); } public virtual bool ThumbnailExists(VideoId videoId,string res) { return FileExists($"Thumbnails/{videoId.Value}/{res}.jpg"); } public virtual async Task ReadThumbnailAsync(VideoId videoId,string res,CancellationToken token=default) { if(await ThumbnailExistsAsync(videoId,res)) { return await ReadAllBytesAsync($"Thumbnails/{videoId.Value}/{res}.jpg",token); } return new byte[0]; } public virtual bool PersonalPlaylistExists(string name) { return FileExists($"PersonalPlaylist/{name}.json"); } public virtual async IAsyncEnumerable GetPersonalPlaylistContentsAsync(string playlist) { if(!PersonalPlaylistExists(playlist)) yield break; var ls=JsonConvert.DeserializeObject>(await ReadAllTextAsync($"PersonalPlaylist/{playlist}.json")); foreach(var item in ls) { yield return await Task.FromResult(item); } } public virtual async IAsyncEnumerable GetPersonalPlaylistsAsync() { await foreach(var item in EnumerateFilesAsync("PersonalPlaylist")) { if(Path.GetExtension(item) == ".json") { yield return await Task.FromResult(Path.GetFileNameWithoutExtension(item)); } } } public virtual async Task<(String Path,bool Delete)> GetRealUrlOrPathAsync(string path) { string tmpFile = Path.GetTempFileName(); File.Delete(tmpFile); tmpFile=tmpFile + Path.GetExtension(path); using(var f = File.Create(tmpFile)) { using(var src=await OpenReadAsync(path)) { await TYTDManager.CopyStream(src,f); } } return (tmpFile,true); } public bool FileExists(string path) { return Task.Run(()=>FileExistsAsync(path)).GetAwaiter().GetResult(); } public virtual async IAsyncEnumerable GetVideoIdsAsync() { await foreach(var item in EnumerateFilesAsync("Info")) { if(Path.GetExtension(item).Equals(".json",StringComparison.Ordinal)) { yield return Path.GetFileNameWithoutExtension(item); } } } public virtual async Task GetVideoInfoAsync(VideoId id) { return JsonConvert.DeserializeObject(await ReadAllTextAsync($"Info/{id}.json")); } public virtual async IAsyncEnumerable GetVideosAsync() { await foreach(var item in GetVideoIdsAsync()) { var item0=await GetVideoInfoAsync(item); if(item0 != null) { yield return item0; } } } public virtual async IAsyncEnumerable GetLegacyVideosAsync() { await foreach(var item in GetVideoIdsAsync()) { var item0 =await GetLegacyVideoInfoAsync(item); if(item0 != null) { yield return item0; } } } public virtual async Task GetLegacyVideoInfoAsync(VideoId id) { return JsonConvert.DeserializeObject(await ReadAllTextAsync($"Info/{id}.json")); } public virtual async IAsyncEnumerable GetPlaylistsAsync() { await foreach(var item in GetPlaylistIdsAsync()) { var item0=await GetPlaylistInfoAsync(item); if(item0 != null) { yield return item0; } } } public async Task ReadAllBytesAsync(string path,CancellationToken token=default(CancellationToken)) {using(var strm = await OpenReadAsync(path)) { byte[] data=new byte[strm.Length]; await strm.ReadAsync(data,0,data.Length,token); if(token.IsCancellationRequested) { return new byte[0]; } return data; } } public virtual async IAsyncEnumerable GetPlaylistIdsAsync() { await foreach(var item in EnumerateFilesAsync("Playlist")) { if(Path.GetExtension(item).Equals(".json",StringComparison.Ordinal)) { yield return Path.GetFileNameWithoutExtension(item); } } } public virtual async IAsyncEnumerable GetChannelIdsAsync() { await foreach(var item in EnumerateFilesAsync("Channel")) { if(Path.GetExtension(item).Equals(".json",StringComparison.Ordinal)) { yield return Path.GetFileNameWithoutExtension(item); } } } public virtual async IAsyncEnumerable GetYouTubeExplodeVideoIdsAsync() { await foreach(var item in GetVideoIdsAsync()) { VideoId? id= VideoId.TryParse(item); if(id.HasValue) yield return id.Value; } } public virtual async Task GetChannelInfoAsync(ChannelId id) { return JsonConvert.DeserializeObject(await ReadAllTextAsync($"Channel/{id}.json")); } public virtual async IAsyncEnumerable GetChannelsAsync() { await foreach(var item in GetChannelIdsAsync()) { var item0=await GetChannelInfoAsync(item); if(item0 != null) { yield return item0; } } } public virtual async IAsyncEnumerable GetDownloadsAsync() { await foreach(var item in EnumerateFilesAsync("DownloadsInfo")) { if(Path.GetExtension(item).Equals(".json",StringComparison.Ordinal)) { yield return JsonConvert.DeserializeObject(await ReadAllTextAsync(item)); } } } public virtual async IAsyncEnumerable GetDownloadUrlsAsync() { await foreach(var item in GetDownloadsAsync()) { yield return item.Id; } } public virtual async Task GetBestStreamInfoAsync(VideoId id) { return JsonConvert.DeserializeObject(await ReadAllTextAsync($"StreamInfo/{id.Value}.json")); } public virtual bool BestStreamInfoExists(VideoId id) { return FileExists($"StreamInfo/{id.Value}.json"); } public virtual async Task GetDownloadInfoAsync(string url) { //string enc=$"FileInfo/{B64.Base64UrlEncodes(url)}.json"; string enc=$"DownloadsInfo/{HashDownloadUrl(url)}.json"; return JsonConvert.DeserializeObject(await ReadAllTextAsync(enc)); } public static string HashDownloadUrl(string url) { using(var sha1=SHA1.Create()){ return B64.Base64UrlEncode(sha1.ComputeHash(Encoding.UTF8.GetBytes(url))); } } public virtual bool DownloadExists(string url) { string enc=$"DownloadsInfo/{HashDownloadUrl(url)}.json"; return FileExists(enc); } public virtual bool PlaylistInfoExists(PlaylistId id) { return FileExists($"Playlist/{id}.json"); } public virtual bool VideoInfoExists(VideoId id) { return FileExists($"Info/{id}.json"); } public virtual bool ChannelInfoExists(ChannelId id) { return FileExists($"Channel/{id}.json"); } public virtual async Task GetPlaylistInfoAsync(PlaylistId id) { return JsonConvert.DeserializeObject(await ReadAllTextAsync($"Playlist/{id}.json")); } public virtual async Task ReadAllTextAsync(string file) { using(var s = await OpenReadAsync(file)) { using(var sr = new StreamReader(s)) { return await sr.ReadToEndAsync(); } } } public virtual bool DirectoryExists(string path) { return Task.Run(()=>DirectoryExistsAsync(path)).GetAwaiter().GetResult(); } public virtual IEnumerable EnumerateFiles(string path) { var e = EnumerateFilesAsync(path).GetAsyncEnumerator(); while(e.MoveNextAsync().GetAwaiter().GetResult()) { yield return e.Current; } } public virtual IEnumerable EnumerateDirectories(string path) { var e = EnumerateDirectoriesAsync(path).GetAsyncEnumerator(); while(e.MoveNextAsync().GetAwaiter().GetResult()) { yield return e.Current; } } public abstract Task OpenReadAsync(string path); public abstract Task FileExistsAsync(string path); public abstract Task DirectoryExistsAsync(string path); public abstract IAsyncEnumerable EnumerateFilesAsync(string path); public abstract IAsyncEnumerable EnumerateDirectoriesAsync(string path); public virtual ExtraData GetExtraData() { ExtraData data=new ExtraData(); return data; } } public class ListContentItem { public ListContentItem() { Id=""; Resolution=Resolution.PreMuxed; } public ListContentItem(VideoId id) { Id=id.Value; Resolution=Resolution.PreMuxed; } public ListContentItem(string id) { Id=id; Resolution =Resolution.PreMuxed; } public ListContentItem(string id,Resolution resolution) { Id=id; Resolution=resolution; } public ListContentItem(VideoId id,Resolution resolution) { Id=id.Value; Resolution=resolution; } public string Id {get;set;} public Resolution Resolution {get;set;} public static implicit operator ListContentItem(VideoId id) { return new ListContentItem(id.Value, Resolution.PreMuxed); } public static implicit operator VideoId?(ListContentItem item) { return VideoId.TryParse(item.Id); } public static implicit operator ListContentItem((VideoId Id,Resolution Resolution) item) { return new ListContentItem (item.Id,item.Resolution); } public static implicit operator (VideoId Id,Resolution Resolution)(ListContentItem item) { return (VideoId.Parse(item.Id),item.Resolution); } } public enum MediaType { Video=0, Playlist=1, Channel=2, } public class SearchResult { public SearchResult() { Title=""; Id=""; Type=MediaType.Video; } public SearchResult(ISearchResult result) { var video = result as VideoSearchResult; var playlist = result as PlaylistSearchResult; var channel = result as ChannelSearchResult; if(video != null) { Id=video.Id; Title = video.Title; Type=MediaType.Video; } if(playlist != null) { Id=playlist.Id; Title=playlist.Title; Type=MediaType.Playlist; } if(channel != null) { Id=channel.Id; Title = channel.Title; Type=MediaType.Channel; } } public string Title {get;set;} public string Id {get;set;} public MediaType Type {get;set;} public void AddToQueue(IStorage storage) { switch(Type) { case MediaType.Video: storage.AddVideoAsync(Id,Resolution.NoDownload); break; case MediaType.Playlist: storage.AddPlaylistAsync(Id,Resolution.NoDownload); break; case MediaType.Channel: storage.AddChannelAsync(Id,Resolution.NoDownload); break; } } } public static class TYTDManager { public static async Task VideoExistsAsync(this ITYTDBase baseCtl,VideoId id,Resolution resolution=Resolution.PreMuxed) { return (await GetVideoPathAsync(baseCtl,id,resolution)).Exists; } public static async Task<(bool Exists,string Path)> GetVideoPathAsync(this ITYTDBase baseCtl,VideoId id,Resolution resolution=Resolution.PreMuxed) { if(!baseCtl.VideoInfoExists(id) || !baseCtl.BestStreamInfoExists(id)) return (false,""); var video=await baseCtl.GetVideoInfoAsync(id); var name = await BestStreams.GetPathResolution(baseCtl,video,resolution); if(string.IsNullOrWhiteSpace(name)) return (false,""); return (await baseCtl.FileExistsAsync(name),name); } public static async IAsyncEnumerable SearchYouTubeAsync(this IStorage storage, string query,bool getMediaInfo=true) { await foreach(var vid in storage.YoutubeClient.Search.GetResultsAsync(query)) { var res=new SearchResult(vid); if(getMediaInfo) { res.AddToQueue(storage); } yield return res; } } public static bool DownloadFileExists(this ITYTDBase baseCtl,string url) { return baseCtl.FileExists(GetDownloadFile(url)); } public static string GetDownloadFile(this ITYTDBase baseCtl,string url) { return GetDownloadFile(url); } public static string GetDownloadFile(string url) { string file_path = $"Downloads/{TYTDBase.HashDownloadUrl(url)}.bin"; return file_path; } /// /// Add Video, Playlist, Channel Or Username /// /// Video, Playlist, Channel Or UserName Url Or Id /// Video Resolution public static async Task> ToPersonalPlaylist(this IAsyncEnumerable list,Resolution res) { List<(VideoId Id,Resolution Resolution)> items=new List<(VideoId Id, Resolution Resolution)>(); await foreach(var item in list) { items.Add((item,res)); } return items; } public static async Task AddItemAsync(this IDownloader downloader,string url,Resolution resolution=Resolution.PreMuxed) { VideoId? vid = VideoId.TryParse(url); PlaylistId? pid = PlaylistId.TryParse(url); ChannelId? cid = ChannelId.TryParse(url); ChannelSlug? slug = ChannelSlug.TryParse(url); ChannelHandle? handle = ChannelHandle.TryParse(url); UserName? user = UserName.TryParse(url); if (url.Length == 11) { if (vid.HasValue) { await downloader.AddVideoAsync(vid.Value, resolution); //shall we download video } } else { if (pid.HasValue) { await downloader.AddPlaylistAsync(pid.Value, resolution); } else if (vid.HasValue) { await downloader.AddVideoAsync(vid.Value, resolution); } else if (cid.HasValue) { await downloader.AddChannelAsync(cid.Value, resolution); } else if(handle.HasValue) { await downloader.AddHandleAsync(handle.Value, resolution); }else if(slug.HasValue) { await downloader.AddSlugAsync(slug.Value,resolution); } else if (user.HasValue) { await downloader.AddUserAsync(user.Value, resolution); } } } internal static void Print(this IProgress prog,string text) { if(prog !=null) { prog.Report(text); } } private static string[] resStr = new string[] {"Muxed","PreMuxed","AudioOnly","VideoOnly"}; /// /// Convert DirectoryName to Resolution /// /// /// Video Resolution public static Resolution DirectoryToResolution(string folder) { int e= Array.IndexOf(resStr,folder); if(e == -1) {return Resolution.NoDownload;} return (Resolution)e; } public static string ResolutionToDirectory(Resolution res) { return resStr[(int)res]; } public static async Task CopyVideoToFileAsync(VideoId id,ITYTDBase src,Stream destFile,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken)) { string infoFile=$"Info/{id.Value}.json"; string path=""; if(await src.FileExistsAsync(infoFile)){ string f=await src.ReadAllTextAsync(infoFile); SavedVideo video = JsonConvert.DeserializeObject(f); path=await BestStreams.GetPathResolution(src,video,res); }else{ return false; } if(string.IsNullOrWhiteSpace(path)) return false; bool ret=false; if(await src.FileExistsAsync(path)) { using(var srcFile = await src.OpenReadAsync(path)) { double len=srcFile.Length; ret= await CopyStream(srcFile,destFile,new Progress((e)=>{ if(progress !=null) { progress.Report(e/len); } }),token); } } return ret; } public static async Task CopyVideoToFileAsync(VideoId id,ITYTDBase src,string dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken)) { bool delete=false; using(var f = File.Create(dest)) { delete=!await CopyVideoToFileAsync(id,src,f,res,progress,token); } if(delete) { File.Delete(dest); } } public static async Task CopyStream(Stream src,Stream dest,IProgress progress=null,CancellationToken token=default(CancellationToken)) { try{ byte[] array=new byte[1024]; int read=0; long totrd=0; do{ read=await src.ReadAsync(array,0,array.Length,token); if(token.IsCancellationRequested) { return false; } totrd += read; await dest.WriteAsync(array,0,read,token); if(token.IsCancellationRequested) { return false; } if(progress!=null) { progress.Report(totrd); } }while(read> 0); }catch(Exception ex) { _=ex; return false; } return true; } public static async Task CopyThumbnailsAsync(string id,ITYTDBase src,IStorage dest,IProgress progress=null,CancellationToken token=default(CancellationToken)) { if(!src.DirectoryExists("Thumbnails") || !src.DirectoryExists($"Thumbnails/{id}")) return; await dest.CopyDirectoryFrom(src,$"Thumbnails/{id}",$"Thumbnails/{id}",progress,token); } public static async Task CopyDirectoryFrom(this IStorage _dest,ITYTDBase _src,string src,string dest,IProgress progress,CancellationToken token=default(CancellationToken)) { List dirs=new List(); await foreach(var dir in _src.EnumerateDirectoriesAsync(src)) { dirs.Add(dir); _dest.CreateDirectoryIfNotExist($"{dest.TrimEnd('/')}/{dir.TrimStart('/')}"); } List files=new List(); await foreach(var file in _src.EnumerateFilesAsync(src)) { files.Add(file); } int total = dirs.Count + files.Count; int i=0; double segLen = 1.0 / total; foreach(var item in dirs) { if(token.IsCancellationRequested) return; await CopyDirectoryFrom(_dest,_src,$"{src.TrimEnd('/')}/{item.TrimStart('/')}",$"{dest.TrimEnd('/')}/{item.TrimStart('/')}",new Progress(e=>{ double percent = e / total; percent += segLen * i; if(percent >= 1) percent=1; if(percent <= 0) percent= 0; progress?.Report(percent); }),token); i++; } if(token.IsCancellationRequested) return; foreach(var item in files) { if(token.IsCancellationRequested) return; await CopyFileFrom(_dest,_src,$"{src.TrimEnd('/')}/{item.TrimStart('/')}",$"{dest.TrimEnd('/')}/{item.TrimStart('/')}",new Progress(e=>{ double percent = e / total; percent += segLen * i; if(percent >= 1) percent=1; if(percent <= 0) percent= 0; progress?.Report(percent); }),token); i++; } } public static async Task CopyFileFrom(this IStorage _dest,ITYTDBase _src,string src,string dest,IProgress progress=null,CancellationToken token=default(CancellationToken)) { using(var srcFile = await _src.OpenReadAsync(src)) {double len=srcFile.Length; bool deleteFile=false; using(var destFile = await _dest.CreateAsync(dest)) { deleteFile=!await CopyStream(srcFile,destFile,new Progress((e)=>{ if(progress !=null) { progress.Report(e/len); } }),token); } //dest.DeleteFile(path); } } public static async Task CopyVideoAsync(VideoId id,ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken)) { await CopyVideoAsync(id,src,dest,res,progress,token,true); } public static async Task CopyChannelContentsAsync(ChannelId id,ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken),bool copyThumbnails=true) { List<(VideoId Id, Resolution Resolution)> items=new List<(VideoId Id, Resolution Resolution)>(); await foreach(var v in src.GetVideosAsync()) { if(v.AuthorChannelId == id.Value) { VideoId? id2=VideoId.TryParse(v.Id); if(id2.HasValue) { items.Add((id2.Value,res)); } } } await CopyPersonalPlaylistContentsAsync(items,src,dest,progress,token,copyThumbnails); } public static async Task CopyChannelAsync(ChannelId id,ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken),bool copyThumbnails=true,bool copyContents=false) { if(!src.ChannelInfoExists(id)) return; var channel = await src.GetChannelInfoAsync(id); await dest.WriteChannelInfoAsync(channel); if(copyThumbnails) await CopyThumbnailsAsync(id,src,dest,progress,default(CancellationToken)); if(copyContents) await CopyChannelContentsAsync(id,src,dest,res,progress,token,copyThumbnails); } public static async Task CopyEverythingAsync(ITYTDBase src,IStorage dest,Resolution res,IProgress progress=null,CancellationToken token=default(CancellationToken),bool copyThumbnails=true) { await CopyAllPlaylistsAsync(src,dest,res,null,token,copyThumbnails,false); if(token.IsCancellationRequested) return; await CopyAllChannelsAsync(src,dest,res,null,token,true); if(token.IsCancellationRequested) return; await CopyAllVideosAsync(src,dest,res,progress,token,copyThumbnails); } public static async Task CopyEverythingAsync(ITYTDBase src,IStorage dest,IProgress progress=null,CancellationToken token=default(CancellationToken),bool copyThumbnails=true) { await CopyEverythingAsync(src,dest,Resolution.Mux,new Progress(e=>{ double percent=e / 4; progress?.Report(percent); })); await CopyEverythingAsync(src,dest,Resolution.PreMuxed,new Progress(e=>{ double percent=e / 4; percent+=0.25; progress?.Report(percent); })); await CopyEverythingAsync(src,dest,Resolution.AudioOnly,new Progress(e=>{ double percent=e / 4; percent+=0.50; progress?.Report(percent); })); await CopyEverythingAsync(src,dest,Resolution.VideoOnly,new Progress(e=>{ double percent=e / 4; percent+=0.75; progress?.Report(percent); })); } public static async Task CopyAllVideosAsync(ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken),bool copyThumbnails=true) { await CopyPersonalPlaylistContentsAsync(await src.GetYouTubeExplodeVideoIdsAsync().ToPersonalPlaylist(res),src,dest,progress,token,copyThumbnails); } public static async Task CopyAllPlaylistsAsync(ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken),bool copyThumbnails=true,bool copyContents=true) { List pl=new List(); await foreach(var playlist in src.GetPlaylistsAsync()) { pl.Add(playlist); } int total=pl.Count; int i = 0; double segLen = 1.0 / total; foreach(var item in pl) { if(token.IsCancellationRequested) return; await CopyPlaylistAsync(item.Id,src,dest,res,new Progress(e=>{ double percent = e / total; percent += segLen * i; if(percent >= 1) percent=1; if(percent <= 0) percent= 0; progress?.Report(percent); }),token,copyThumbnails,copyContents); i++; } } public static async Task CopyAllChannelsAsync(ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken),bool copyThumbnails=true,bool copyContents=false) { //copying all channels is equivalent to copying all videos except we copy all channels info also await foreach(var item in src.GetChannelsAsync()) { await dest.WriteChannelInfoAsync(item); } if(copyContents) await CopyAllVideosAsync(src,dest,res,progress,token,copyThumbnails); } public static async Task CopyPersonalPlaylistContentsAsync(List<(VideoId Id,Resolution Resolution)> list,ITYTDBase src,IStorage dest,IProgress progress=null,CancellationToken token=default(CancellationToken), bool copyThumbnails=true) { int total=list.Count; int i = 0; double segLen = 1.0 / total; foreach(var item in list) { if(token.IsCancellationRequested) return; await CopyVideoAsync(item.Id,src,dest,item.Resolution,new Progress(e=>{ double percent = e / total; percent += segLen * i; if(percent >= 1) percent=1; if(percent <= 0) percent= 0; progress?.Report(percent); }),token,copyThumbnails); i++; } } public static async Task CopyPlaylistContentsAsync(SavedPlaylist list,ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken), bool copyThumbnails=true) { await CopyPersonalPlaylistContentsAsync(list.ToPersonalPlaylist(res),src,dest,progress,token,copyThumbnails); } public static async Task CopyPlaylistAsync(PlaylistId id,ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token = default(CancellationToken),bool copyThumbnails=true,bool copyContents=true) { //copy playlist and videos if(!src.PlaylistInfoExists(id)) return; var playlist=await src.GetPlaylistInfoAsync(id); await dest.WritePlaylistInfoAsync(playlist); if(copyContents) await CopyPlaylistContentsAsync(playlist,src,dest,res,progress,token,copyThumbnails); } public static async Task CopyVideoAsync(VideoId id,ITYTDBase src,IStorage dest,Resolution res=Resolution.PreMuxed,IProgress progress=null,CancellationToken token=default(CancellationToken),bool copyThumbnails=true) { if(copyThumbnails) { await CopyThumbnailsAsync(id.Value,src,dest,progress,token); } string resDir = ResolutionToDirectory(res); string infoFile = $"Info/{id}.json"; string streamInfo = $"StreamInfo/{id}.json"; string path=""; if(src.VideoInfoExists(id)) { SavedVideo video = await src.GetVideoInfoAsync(id); path=await BestStreams.GetPathResolution(src,video,res); if(!dest.VideoInfoExists(id)) { await dest.WriteVideoInfoAsync(video); } }else{ return; } if(string.IsNullOrWhiteSpace(path)) return; if(await src.FileExistsAsync(streamInfo)) { if(!await dest.FileExistsAsync(streamInfo)) { string f = await src.ReadAllTextAsync(streamInfo); await dest.WriteAllTextAsync(infoFile,f); } } if(token.IsCancellationRequested) { return; } if(await src.FileExistsAsync(path)) { if(!await dest.FileExistsAsync(path)) { return; } dest.CreateDirectory(resDir); using(var srcFile = await src.OpenReadAsync(path)) { double len=srcFile.Length; bool deleteFile=false; using(var destFile = await dest.CreateAsync(path)) { deleteFile=!await CopyStream(srcFile,destFile,new Progress((e)=>{ if(progress !=null) { progress.Report(e/len); } }),token); } //dest.DeleteFile(path); } } } } public class Chapter { public Chapter(TimeSpan ts, string name) { Offset = ts; ChapterName = name; } public TimeSpan Length {get;set;} public TimeSpan Offset { get; set; } public string ChapterName { get; set; } public override string ToString() { return $"[{Offset}-{Offset.Add(Length)}] {ChapterName}"; } } public interface IPersonalPlaylistGet { ExtraData GetExtraData(); IAsyncEnumerable GetPersonalPlaylistContentsAsync(string name); bool PersonalPlaylistExists(string name); } public interface IPersonalPlaylistSet : IPersonalPlaylistGet { Task AddToPersonalPlaylistAsync(string name, IEnumerable items); Task ReplacePersonalPlaylistAsync(string name,IEnumerable items); Task RemoveItemFromPersonalPlaylistAsync(string name,VideoId id); Task SetResolutionForItemInPersonalPlaylistAsync(string name,VideoId id,Resolution resolution); } public interface IWritable : IPersonalPlaylistGet, IPersonalPlaylistSet { public Task WriteAllTextAsync(string path,string data); } }