diff --git a/Kyoo.Common/.gitignore b/Kyoo.Common/.gitignore new file mode 100644 index 00000000..877ccf42 --- /dev/null +++ b/Kyoo.Common/.gitignore @@ -0,0 +1,233 @@ +## PROJECT CUSTOM IGNORES + +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +bin/ +Bin/ +obj/ +Obj/ + +# Visual Studio 2015 cache/options directory +.vs/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directory +AppPackages/ +BundleArtifacts/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +orleans.codegen.cs + +/node_modules + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ diff --git a/Kyoo/InternalAPI/Crawler/ICrawler.cs b/Kyoo.Common/Controllers/ICrawler.cs similarity index 55% rename from Kyoo/InternalAPI/Crawler/ICrawler.cs rename to Kyoo.Common/Controllers/ICrawler.cs index 30514f7f..6dc29922 100644 --- a/Kyoo/InternalAPI/Crawler/ICrawler.cs +++ b/Kyoo.Common/Controllers/ICrawler.cs @@ -1,12 +1,12 @@ using System.Threading; using System.Threading.Tasks; -namespace Kyoo.InternalAPI +namespace Kyoo.Controllers { public interface ICrawler { - Task Start(bool watch); + void Start(); - Task StopAsync(); + void Cancel(); } } diff --git a/Kyoo/InternalAPI/LibraryManager/ILibraryManager.cs b/Kyoo.Common/Controllers/ILibraryManager.cs similarity index 94% rename from Kyoo/InternalAPI/LibraryManager/ILibraryManager.cs rename to Kyoo.Common/Controllers/ILibraryManager.cs index a85cceb3..b920219b 100644 --- a/Kyoo/InternalAPI/LibraryManager/ILibraryManager.cs +++ b/Kyoo.Common/Controllers/ILibraryManager.cs @@ -2,7 +2,7 @@ using Kyoo.Models.Watch; using System.Collections.Generic; -namespace Kyoo.InternalAPI +namespace Kyoo.Controllers { public interface ILibraryManager { @@ -63,9 +63,9 @@ namespace Kyoo.InternalAPI long GetOrCreateGenre(Genre genre); long GetOrCreateStudio(Studio studio); - void RegisterShowPeople(long showID, List actors); + void RegisterShowPeople(long showID, IEnumerable actors); void AddShowToCollection(long showID, long collectionID); - void RegisterInLibrary(long showID, string libraryPath); + void RegisterInLibrary(long showID, Library library); void RemoveEpisode(Episode episode); void ClearSubtitles(long episodeID); diff --git a/Kyoo.Common/Controllers/IMetadataProvider.cs b/Kyoo.Common/Controllers/IMetadataProvider.cs new file mode 100644 index 00000000..0813418c --- /dev/null +++ b/Kyoo.Common/Controllers/IMetadataProvider.cs @@ -0,0 +1,25 @@ +using Kyoo.Models; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Kyoo.Controllers +{ + public interface IMetadataProvider + { + public string Name { get; } + + //For the collection + Task GetCollectionFromName(string name); + + //For the show + Task GetShowByID(string id); + Task GetShowFromName(string showName); + Task> GetPeople(Show show); + + //For the seasons + Task GetSeason(Show show, long seasonNumber); + + //For the episodes + Task GetEpisode(Show show, long seasonNumber, long episodeNumber, long absoluteNumber); + } +} diff --git a/Kyoo.Common/Controllers/IPluginManager.cs b/Kyoo.Common/Controllers/IPluginManager.cs new file mode 100644 index 00000000..815c8395 --- /dev/null +++ b/Kyoo.Common/Controllers/IPluginManager.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace Kyoo.Controllers +{ + public interface IPluginManager + { + public T GetPlugin(string name); + public IEnumerable GetPlugins(); + public void ReloadPlugins(); + } +} \ No newline at end of file diff --git a/Kyoo.Common/Controllers/IProviderManager.cs b/Kyoo.Common/Controllers/IProviderManager.cs new file mode 100644 index 00000000..0da49676 --- /dev/null +++ b/Kyoo.Common/Controllers/IProviderManager.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Kyoo.Models; + +namespace Kyoo.Controllers +{ + public interface IProviderManager + { + Task GetCollectionFromName(string name, Library library); + Task GetShowFromName(string showName, Library library); + Task GetSeason(Show show, long seasonNumber, Library library); + Task GetEpisode(Show show, long seasonNumber, long episodeNumber, long absoluteNumber, Library library); + Task> GetPeople(Show show, Library library); + } +} \ No newline at end of file diff --git a/Kyoo/InternalAPI/ThumbnailsManager/IThumbnailsManager.cs b/Kyoo.Common/Controllers/IThumbnailsManager.cs similarity index 66% rename from Kyoo/InternalAPI/ThumbnailsManager/IThumbnailsManager.cs rename to Kyoo.Common/Controllers/IThumbnailsManager.cs index 4d11d7f3..4144f1ec 100644 --- a/Kyoo/InternalAPI/ThumbnailsManager/IThumbnailsManager.cs +++ b/Kyoo.Common/Controllers/IThumbnailsManager.cs @@ -2,12 +2,12 @@ using System.Collections.Generic; using System.Threading.Tasks; -namespace Kyoo.InternalAPI.ThumbnailsManager +namespace Kyoo.Controllers.ThumbnailsManager { public interface IThumbnailsManager { Task Validate(Show show); - Task> Validate(List actors); + Task> Validate(IEnumerable actors); Task Validate(Episode episode); } } diff --git a/Kyoo/InternalAPI/Transcoder/ITranscoder.cs b/Kyoo.Common/Controllers/ITranscoder.cs similarity index 96% rename from Kyoo/InternalAPI/Transcoder/ITranscoder.cs rename to Kyoo.Common/Controllers/ITranscoder.cs index bea35952..861cd492 100644 --- a/Kyoo/InternalAPI/Transcoder/ITranscoder.cs +++ b/Kyoo.Common/Controllers/ITranscoder.cs @@ -2,7 +2,7 @@ using Kyoo.Models; using Kyoo.Models.Watch; using System.Threading.Tasks; -namespace Kyoo.InternalAPI +namespace Kyoo.Controllers { public interface ITranscoder { diff --git a/Kyoo.Common/Kyoo.Common.csproj b/Kyoo.Common/Kyoo.Common.csproj new file mode 100644 index 00000000..131dda1e --- /dev/null +++ b/Kyoo.Common/Kyoo.Common.csproj @@ -0,0 +1,22 @@ + + + + netcoreapp3.0 + true + Kyoo.Common + Anonymus Raccoon + Base package to create plugins for Kyoo. + https://github.com/AnonymusRaccoon/Kyoo + https://github.com/AnonymusRaccoon/Kyoo + SDG + GPL-3.0-or-later + true + 1.0.6 + + + + + + + + diff --git a/Kyoo.Common/Models/Collection.cs b/Kyoo.Common/Models/Collection.cs new file mode 100644 index 00000000..74d63518 --- /dev/null +++ b/Kyoo.Common/Models/Collection.cs @@ -0,0 +1,75 @@ +using Kyoo.Controllers; +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; + +namespace Kyoo.Models +{ + public class Collection : IMergable + + { + [JsonIgnore] public long ID = -1; + public string Slug; + public string Name; + public string Poster; + public string Overview; + [JsonIgnore] public string ImgPrimary; + public IEnumerable Shows; + + public Collection() { } + + public Collection(long id, string slug, string name, string overview, string imgPrimary) + { + ID = id; + Slug = slug; + Name = name; + Overview = overview; + ImgPrimary = imgPrimary; + } + + public static Collection FromReader(System.Data.SQLite.SQLiteDataReader reader) + { + Collection col = new Collection((long) reader["id"], + reader["slug"] as string, + reader["name"] as string, + reader["overview"] as string, + reader["imgPrimary"] as string); + col.Poster = "poster/" + col.Slug; + return col; + } + + public Show AsShow() + { + return new Show(-1, Slug, Name, null, null, Overview, null, null, null, null, null, null); + } + + public Collection SetShows(ILibraryManager libraryManager) + { + Shows = libraryManager.GetShowsInCollection(ID); + return this; + } + + public Collection Merge(Collection collection) + { + if (collection == null) + return this; + if (ID == -1) + ID = collection.ID; + if (Slug == null) + Slug = collection.Slug; + if (Name == null) + Name = collection.Name; + if (Poster == null) + Poster = collection.Poster; + if (Overview == null) + Overview = collection.Overview; + if (ImgPrimary == null) + ImgPrimary = collection.ImgPrimary; + if (Shows == null) + Shows = collection.Shows; + else + Shows = Shows.Concat(collection.Shows); + return this; + } + } +} diff --git a/Kyoo/Models/Episode.cs b/Kyoo.Common/Models/Episode.cs similarity index 59% rename from Kyoo/Models/Episode.cs rename to Kyoo.Common/Models/Episode.cs index 58661490..3ad3aada 100644 --- a/Kyoo/Models/Episode.cs +++ b/Kyoo.Common/Models/Episode.cs @@ -3,15 +3,15 @@ using System; namespace Kyoo.Models { - public class Episode + public class Episode : IMergable { - [JsonIgnore] public long id; + [JsonIgnore] public long ID; [JsonIgnore] public long ShowID; [JsonIgnore] public long SeasonID; - public long seasonNumber; - public long episodeNumber; - public long absoluteNumber; + public long SeasonNumber; + public long EpisodeNumber; + public long AbsoluteNumber; [JsonIgnore] public string Path; public string Title; public string Overview; @@ -27,16 +27,24 @@ namespace Kyoo.Models public string Thumb; //Used in the API response only - public Episode() { } + public Episode() + { + ID = -1; + ShowID = -1; + SeasonID = -1; + SeasonNumber = -1; + EpisodeNumber = -1; + AbsoluteNumber = -1; + } public Episode(long seasonNumber, long episodeNumber, long absoluteNumber, string title, string overview, DateTime? releaseDate, long runtime, string imgPrimary, string externalIDs) { - id = -1; + ID = -1; ShowID = -1; SeasonID = -1; - this.seasonNumber = seasonNumber; - this.episodeNumber = episodeNumber; - this.absoluteNumber = absoluteNumber; + SeasonNumber = seasonNumber; + EpisodeNumber = episodeNumber; + AbsoluteNumber = absoluteNumber; Title = title; Overview = overview; ReleaseDate = releaseDate; @@ -47,12 +55,12 @@ namespace Kyoo.Models public Episode(long id, long showID, long seasonID, long seasonNumber, long episodeNumber, long absoluteNumber, string path, string title, string overview, DateTime? releaseDate, long runtime, string imgPrimary, string externalIDs) { - this.id = id; + ID = id; ShowID = showID; SeasonID = seasonID; - this.seasonNumber = seasonNumber; - this.episodeNumber = episodeNumber; - this.absoluteNumber = absoluteNumber; + SeasonNumber = seasonNumber; + EpisodeNumber = episodeNumber; + AbsoluteNumber = absoluteNumber; Path = path; Title = title; Overview = overview; @@ -82,7 +90,7 @@ namespace Kyoo.Models public Episode SetThumb(string showSlug) { - Link = GetSlug(showSlug, seasonNumber, episodeNumber); + Link = GetSlug(showSlug, SeasonNumber, EpisodeNumber); Thumb = "thumb/" + Link; return this; } @@ -97,5 +105,37 @@ namespace Kyoo.Models { return showSlug + "-s" + seasonNumber + "e" + episodeNumber; } + + public Episode Merge(Episode other) + { + if (other == null) + return this; + if (ID == -1) + ID = other.ID; + if (ShowID == -1) + ShowID = other.ShowID; + if (SeasonID == -1) + SeasonID = other.SeasonID; + if (SeasonNumber == -1) + SeasonNumber = other.SeasonNumber; + if (EpisodeNumber == -1) + EpisodeNumber = other.EpisodeNumber; + if (AbsoluteNumber == -1) + AbsoluteNumber = other.AbsoluteNumber; + if (Path == null) + Path = other.Path; + if (Title == null) + Title = other.Title; + if (Overview == null) + Overview = other.Overview; + if (ReleaseDate == null) + ReleaseDate = other.ReleaseDate; + if (Runtime == -1) + Runtime = other.Runtime; + if (ImgPrimary == null) + ImgPrimary = other.ImgPrimary; + ExternalIDs += '|' + other.ExternalIDs; + return this; + } } } diff --git a/Kyoo/Models/Genre.cs b/Kyoo.Common/Models/Genre.cs similarity index 89% rename from Kyoo/Models/Genre.cs rename to Kyoo.Common/Models/Genre.cs index 93ff9d8f..e80fa2c0 100644 --- a/Kyoo/Models/Genre.cs +++ b/Kyoo.Common/Models/Genre.cs @@ -4,7 +4,7 @@ namespace Kyoo.Models { public class Genre { - [JsonIgnore] public readonly long id; + [JsonIgnore] public readonly long ID; public string Slug; public string Name; @@ -16,7 +16,7 @@ namespace Kyoo.Models public Genre(long id, string slug, string name) { - this.id = id; + ID = id; Slug = slug; Name = name; } diff --git a/Kyoo.Common/Models/ImageTypes.cs b/Kyoo.Common/Models/ImageTypes.cs new file mode 100644 index 00000000..fc643362 --- /dev/null +++ b/Kyoo.Common/Models/ImageTypes.cs @@ -0,0 +1,4 @@ +namespace Kyoo.Models +{ + public enum ImageType { Poster, Background, Thumbnail, Logo } +} \ No newline at end of file diff --git a/Kyoo/Models/Library.cs b/Kyoo.Common/Models/Library.cs similarity index 60% rename from Kyoo/Models/Library.cs rename to Kyoo.Common/Models/Library.cs index 638091c9..6b4db9af 100644 --- a/Kyoo/Models/Library.cs +++ b/Kyoo.Common/Models/Library.cs @@ -4,17 +4,19 @@ namespace Kyoo.Models { public class Library { - [JsonIgnore] public readonly long id; + [JsonIgnore] public readonly long ID; public string Slug; public string Name; - public string Path; + public string[] Paths; + public string[] Providers; - public Library(long id, string slug, string name, string path) + public Library(long id, string slug, string name, string[] paths, string[] providers) { - this.id = id; + ID = id; Slug = slug; Name = name; - Path = path; + Paths = paths; + Providers = providers; } public static Library FromReader(System.Data.SQLite.SQLiteDataReader reader) @@ -22,7 +24,8 @@ namespace Kyoo.Models return new Library((long)reader["id"], reader["slug"] as string, reader["name"] as string, - reader["path"] as string); + (reader["path"] as string)?.Split('|'), + (reader["providers"] as string)?.Split('|')); } } } diff --git a/Kyoo/Models/People.cs b/Kyoo.Common/Models/People.cs similarity index 56% rename from Kyoo/Models/People.cs rename to Kyoo.Common/Models/People.cs index 79003c23..4c189f99 100644 --- a/Kyoo/Models/People.cs +++ b/Kyoo.Common/Models/People.cs @@ -2,35 +2,37 @@ namespace Kyoo.Models { - public class People + public class People : IMergable { - [JsonIgnore] public long id; - public string slug; + [JsonIgnore] public long ID = -1; + public string Slug; public string Name; public string Role; //Dynamic data not stored as it in the database public string Type; //Dynamic data not stored as it in the database ---- Null for now - [JsonIgnore] public string imgPrimary; + [JsonIgnore] public string ImgPrimary; - public string externalIDs; + public string ExternalIDs; + + public People() {} public People(long id, string slug, string name, string imgPrimary, string externalIDs) { - this.id = id; - this.slug = slug; + ID = id; + Slug = slug; Name = name; - this.imgPrimary = imgPrimary; - this.externalIDs = externalIDs; + ImgPrimary = imgPrimary; + ExternalIDs = externalIDs; } public People(long id, string slug, string name, string role, string type, string imgPrimary, string externalIDs) { - this.id = id; - this.slug = slug; + ID = id; + Slug = slug; Name = name; Role = role; Type = type; - this.imgPrimary = imgPrimary; - this.externalIDs = externalIDs; + ImgPrimary = imgPrimary; + ExternalIDs = externalIDs; } public static People FromReader(System.Data.SQLite.SQLiteDataReader reader) @@ -52,5 +54,25 @@ namespace Kyoo.Models reader["imgPrimary"] as string, reader["externalIDs"] as string); } + + public People Merge(People other) + { + if (other == null) + return this; + if (ID == -1) + ID = other.ID; + if (Slug == null) + Slug = other.Slug; + if (Name == null) + Name = other.Name; + if (Role == null) + Role = other.Role; + if (Type == null) + Type = other.Type; + if (ImgPrimary == null) + ImgPrimary = other.ImgPrimary; + ExternalIDs += '|' + other.ExternalIDs; + return this; + } } } diff --git a/Kyoo.Common/Models/Plugin.cs b/Kyoo.Common/Models/Plugin.cs new file mode 100644 index 00000000..c79de89c --- /dev/null +++ b/Kyoo.Common/Models/Plugin.cs @@ -0,0 +1,7 @@ +namespace Kyoo.Models +{ + public interface IPlugin + { + public string Name { get; } + } +} \ No newline at end of file diff --git a/Kyoo.Common/Models/SearchResult.cs b/Kyoo.Common/Models/SearchResult.cs new file mode 100644 index 00000000..306814b8 --- /dev/null +++ b/Kyoo.Common/Models/SearchResult.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace Kyoo.Models +{ + public class SearchResult + { + public string Query; + public IEnumerable Shows; + public IEnumerable Episodes; + public IEnumerable People; + public IEnumerable Genres; + public IEnumerable Studios; + } +} diff --git a/Kyoo/Models/Season.cs b/Kyoo.Common/Models/Season.cs similarity index 54% rename from Kyoo/Models/Season.cs rename to Kyoo.Common/Models/Season.cs index c555d932..5ff2bee5 100644 --- a/Kyoo/Models/Season.cs +++ b/Kyoo.Common/Models/Season.cs @@ -2,15 +2,15 @@ namespace Kyoo.Models { - public class Season + public class Season : IMergable { - [JsonIgnore] public readonly long id; - [JsonIgnore] public long ShowID; + [JsonIgnore] public readonly long ID = -1; + [JsonIgnore] public long ShowID = -1; - public long seasonNumber; + public long SeasonNumber = -1; public string Title; public string Overview; - public long? year; + public long? Year; [JsonIgnore] public string ImgPrimary; public string ExternalIDs; @@ -19,12 +19,12 @@ namespace Kyoo.Models public Season(long id, long showID, long seasonNumber, string title, string overview, long? year, string imgPrimary, string externalIDs) { - this.id = id; + ID = id; ShowID = showID; - this.seasonNumber = seasonNumber; + SeasonNumber = seasonNumber; Title = title; Overview = overview; - this.year = year; + Year = year; ImgPrimary = imgPrimary; ExternalIDs = externalIDs; } @@ -40,5 +40,25 @@ namespace Kyoo.Models reader["imgPrimary"] as string, reader["externalIDs"] as string); } + + public Season Merge(Season other) + { + if (other == null) + return this; + if (ShowID == -1) + ShowID = other.ShowID; + if (SeasonNumber == -1) + SeasonNumber = other.SeasonNumber; + if (Title == null) + Title = other.Title; + if (Overview == null) + Overview = other.Overview; + if (Year == null) + Year = other.Year; + if (ImgPrimary == null) + ImgPrimary = other.ImgPrimary; + ExternalIDs += '|' + other.ExternalIDs; + return this; + } } } diff --git a/Kyoo/Models/Show.cs b/Kyoo.Common/Models/Show.cs similarity index 62% rename from Kyoo/Models/Show.cs rename to Kyoo.Common/Models/Show.cs index b2c267ee..00cbfafa 100644 --- a/Kyoo/Models/Show.cs +++ b/Kyoo.Common/Models/Show.cs @@ -1,12 +1,14 @@ -using Kyoo.InternalAPI; +using System; +using Kyoo.Controllers; using Newtonsoft.Json; using System.Collections.Generic; +using System.Linq; namespace Kyoo.Models { - public class Show + public class Show : IMergable { - [JsonIgnore] public long id = -1; + [JsonIgnore] public long ID = -1; public string Slug; public string Title; @@ -28,27 +30,21 @@ namespace Kyoo.Models public string ExternalIDs; //Used in the rest API excusively. - public Studio studio; - public IEnumerable directors; - public IEnumerable people; - public IEnumerable seasons; + public Studio Studio; + public IEnumerable Directors; + public IEnumerable People; + public IEnumerable Seasons; public bool IsCollection; public string GetAliases() { - if (Aliases == null) - return null; - - return string.Join('|', Aliases); + return Aliases == null ? null : string.Join('|', Aliases); } public string GetGenres() { - if (Genres == null) - return null; - - return string.Join('|', Genres); + return Genres == null ? null : string.Join('|', Genres); } @@ -56,7 +52,7 @@ namespace Kyoo.Models public Show(long id, string slug, string title, IEnumerable aliases, string path, string overview, string trailerUrl, IEnumerable genres, Status? status, long? startYear, long? endYear, string externalIDs) { - this.id = id; + ID = id; Slug = slug; Title = title; Aliases = aliases; @@ -73,7 +69,7 @@ namespace Kyoo.Models public Show(long id, string slug, string title, IEnumerable aliases, string path, string overview, string trailerUrl, Status? status, long? startYear, long? endYear, string imgPrimary, string imgThumb, string imgLogo, string imgBackdrop, string externalIDs) { - this.id = id; + ID = id; Slug = slug; Title = title; Aliases = aliases; @@ -111,7 +107,7 @@ namespace Kyoo.Models return new Show((long)reader["id"], reader["slug"] as string, reader["title"] as string, - (reader["aliases"] as string)?.Split('|') ?? null, + (reader["aliases"] as string)?.Split('|'), reader["path"] as string, reader["overview"] as string, reader["trailerUrl"] as string, @@ -125,6 +121,16 @@ namespace Kyoo.Models reader["externalIDs"] as string); } + public string GetID(string provider) + { + if (ExternalIDs?.Contains(provider) != true) + return null; + int startIndex = ExternalIDs.IndexOf(provider, StringComparison.Ordinal) + provider.Length + 1; //The + 1 is for the '=' + if (ExternalIDs.IndexOf('|', startIndex) == -1) + return ExternalIDs.Substring(startIndex); + return ExternalIDs.Substring(startIndex, ExternalIDs.IndexOf('|', startIndex) - startIndex); + } + public Show Set(string slug, string path) { Slug = slug; @@ -134,31 +140,73 @@ namespace Kyoo.Models public Show SetGenres(ILibraryManager manager) { - Genres = manager.GetGenreForShow(id); + Genres = manager.GetGenreForShow(ID); return this; } public Show SetStudio(ILibraryManager manager) { - studio = manager.GetStudio(id); + Studio = manager.GetStudio(ID); return this; } public Show SetDirectors(ILibraryManager manager) { - directors = manager.GetDirectors(id); + Directors = manager.GetDirectors(ID); return this; } public Show SetPeople(ILibraryManager manager) { - people = manager.GetPeople(id); + People = manager.GetPeople(ID); return this; } public Show SetSeasons(ILibraryManager manager) { - seasons = manager.GetSeasons(id); + Seasons = manager.GetSeasons(ID); + return this; + } + + public Show Merge(Show other) + { + if (other == null) + return this; + if (ID == -1) + ID = other.ID; + if (Slug == null) + Slug = other.Slug; + if (Title == null) + Title = other.Title; + if (Aliases == null) + Aliases = other.Aliases; + else + Aliases = Aliases.Concat(other.Aliases); + if (Genres == null) + Genres = other.Genres; + else + Genres = Genres.Concat(other.Genres); + if (Path == null) + Path = other.Path; + if (Overview == null) + Overview = other.Overview; + if (TrailerUrl == null) + TrailerUrl = other.TrailerUrl; + if (Status == null) + Status = other.Status; + if (StartYear == null) + StartYear = other.StartYear; + if (EndYear == null) + EndYear = other.EndYear; + if (ImgPrimary == null) + ImgPrimary = other.ImgPrimary; + if (ImgThumb == null) + ImgThumb = other.ImgThumb; + if (ImgLogo == null) + ImgLogo = other.ImgLogo; + if (ImgBackdrop == null) + ImgBackdrop = other.ImgBackdrop; + ExternalIDs += '|' + other.ExternalIDs; return this; } } diff --git a/Kyoo/Models/Studio.cs b/Kyoo.Common/Models/Studio.cs similarity index 91% rename from Kyoo/Models/Studio.cs rename to Kyoo.Common/Models/Studio.cs index 6fea773a..48f6d68c 100644 --- a/Kyoo/Models/Studio.cs +++ b/Kyoo.Common/Models/Studio.cs @@ -4,7 +4,7 @@ namespace Kyoo.Models { public class Studio { - [JsonIgnore] public readonly long id; + [JsonIgnore] public readonly long ID = -1; public string Slug; public string Name; @@ -16,7 +16,7 @@ namespace Kyoo.Models public Studio(long id, string slug, string name) { - this.id = id; + ID = id; Slug = slug; Name = name; } diff --git a/Kyoo/Models/Track.cs b/Kyoo.Common/Models/Track.cs similarity index 97% rename from Kyoo/Models/Track.cs rename to Kyoo.Common/Models/Track.cs index 54f26ee0..6c403fde 100644 --- a/Kyoo/Models/Track.cs +++ b/Kyoo.Common/Models/Track.cs @@ -58,7 +58,7 @@ namespace Kyoo.Models public string DisplayName; public string Link; - [JsonIgnore] public long episodeID; + [JsonIgnore] public long EpisodeID; [JsonIgnore] public bool IsExternal; public Track(StreamType type, string title, string language, bool isDefault, bool isForced, string codec, bool isExternal, string path) @@ -94,7 +94,7 @@ namespace Kyoo.Models if (language == "fre") language = "fra"; - DisplayName = CultureInfo.GetCultures(CultureTypes.NeutralCultures).FirstOrDefault(x => x.ThreeLetterISOLanguageName == language)?.DisplayName ?? language; + DisplayName = CultureInfo.GetCultures(CultureTypes.NeutralCultures).FirstOrDefault(x => x.ThreeLetterISOLanguageName == language)?.EnglishName ?? language; Link = "/subtitle/" + episodeSlug + "." + Language; if (IsForced) diff --git a/Kyoo/Models/WatchItem.cs b/Kyoo.Common/Models/WatchItem.cs similarity index 63% rename from Kyoo/Models/WatchItem.cs rename to Kyoo.Common/Models/WatchItem.cs index 4d092aa4..34eb0964 100644 --- a/Kyoo/Models/WatchItem.cs +++ b/Kyoo.Common/Models/WatchItem.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Newtonsoft.Json; using System; using System.Collections.Generic; @@ -7,33 +7,33 @@ namespace Kyoo.Models { public class WatchItem { - [JsonIgnore] public readonly long episodeID; + [JsonIgnore] public readonly long EpisodeID = -1; public string ShowTitle; public string ShowSlug; - public long seasonNumber; - public long episodeNumber; + public long SeasonNumber; + public long EpisodeNumber; public string Title; public string Link; public DateTime? ReleaseDate; [JsonIgnore] public string Path; - public string previousEpisode; - public Episode nextEpisode; + public string PreviousEpisode; + public Episode NextEpisode; - public string container; - public Track video; - public IEnumerable audios; - public IEnumerable subtitles; + public string Container; + public Track Video; + public IEnumerable Audios; + public IEnumerable Subtitles; public WatchItem() { } public WatchItem(long episodeID, string showTitle, string showSlug, long seasonNumber, long episodeNumber, string title, DateTime? releaseDate, string path) { - this.episodeID = episodeID; + EpisodeID = episodeID; ShowTitle = showTitle; ShowSlug = showSlug; - this.seasonNumber = seasonNumber; - this.episodeNumber = episodeNumber; + SeasonNumber = seasonNumber; + EpisodeNumber = episodeNumber; Title = title; ReleaseDate = releaseDate; Path = path; @@ -43,8 +43,8 @@ namespace Kyoo.Models public WatchItem(long episodeID, string showTitle, string showSlug, long seasonNumber, long episodeNumber, string title, DateTime? releaseDate, string path, Track[] audios, Track[] subtitles) : this(episodeID, showTitle, showSlug, seasonNumber, episodeNumber, title, releaseDate, path) { - this.audios = audios; - this.subtitles = subtitles; + Audios = audios; + Subtitles = subtitles; } public static WatchItem FromReader(System.Data.SQLite.SQLiteDataReader reader) @@ -61,35 +61,35 @@ namespace Kyoo.Models public WatchItem SetStreams(ILibraryManager libraryManager) { - (Track video, IEnumerable audios, IEnumerable subtitles) streams = libraryManager.GetStreams(episodeID, Link); + (Track video, IEnumerable audios, IEnumerable subtitles) streams = libraryManager.GetStreams(EpisodeID, Link); - container = Path.Substring(Path.LastIndexOf('.') + 1); - video = streams.video; - audios = streams.audios; - subtitles = streams.subtitles; + Container = Path.Substring(Path.LastIndexOf('.') + 1); + Video = streams.video; + Audios = streams.audios; + Subtitles = streams.subtitles; return this; } public WatchItem SetPrevious(ILibraryManager libraryManager) { - long lastEp = episodeNumber - 1; + long lastEp = EpisodeNumber - 1; if(lastEp > 0) - previousEpisode = ShowSlug + "-s" + seasonNumber + "e" + lastEp; - else if(seasonNumber > 1) + PreviousEpisode = ShowSlug + "-s" + SeasonNumber + "e" + lastEp; + else if(SeasonNumber > 1) { - int seasonCount = libraryManager.GetSeasonCount(ShowSlug, seasonNumber - 1); - previousEpisode = ShowSlug + "-s" + (seasonNumber - 1) + "e" + seasonCount; + int seasonCount = libraryManager.GetSeasonCount(ShowSlug, SeasonNumber - 1); + PreviousEpisode = ShowSlug + "-s" + (SeasonNumber - 1) + "e" + seasonCount; } return this; } public WatchItem SetNext(ILibraryManager libraryManager) { - long seasonCount = libraryManager.GetSeasonCount(ShowSlug, seasonNumber); - if (episodeNumber >= seasonCount) - nextEpisode = libraryManager.GetEpisode(ShowSlug, seasonNumber + 1, 1); + long seasonCount = libraryManager.GetSeasonCount(ShowSlug, SeasonNumber); + if (EpisodeNumber >= seasonCount) + NextEpisode = libraryManager.GetEpisode(ShowSlug, SeasonNumber + 1, 1); else - nextEpisode = libraryManager.GetEpisode(ShowSlug, seasonNumber, episodeNumber + 1); + NextEpisode = libraryManager.GetEpisode(ShowSlug, SeasonNumber, EpisodeNumber + 1); return this; } diff --git a/Kyoo.Common/Utility.cs b/Kyoo.Common/Utility.cs new file mode 100644 index 00000000..e161e132 --- /dev/null +++ b/Kyoo.Common/Utility.cs @@ -0,0 +1,62 @@ +using System.Text.RegularExpressions; +using Kyoo.Models; + +namespace Kyoo +{ + public interface IMergable + { + public T Merge(T other); + } + + public static class Utility + { + public static string ToSlug(string name) + { + if (name == null) + return null; + + //First to lower case + name = name.ToLowerInvariant(); + + //Remove all accents + //var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(showTitle); + //showTitle = Encoding.ASCII.GetString(bytes); + + //Replace spaces + name = Regex.Replace(name, @"\s", "-", RegexOptions.Compiled); + + //Remove invalid chars + name = Regex.Replace(name, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); + + //Trim dashes from end + name = name.Trim('-', '_'); + + //Replace double occurences of - or \_ + name = Regex.Replace(name, @"([-_]){2,}", "$1", RegexOptions.Compiled); + + return name; + } + + + public static void SetImage(Show show, string imgUrl, ImageType type) + { + switch(type) + { + case ImageType.Poster: + show.ImgPrimary = imgUrl; + break; + case ImageType.Thumbnail: + show.ImgThumb = imgUrl; + break; + case ImageType.Logo: + show.ImgLogo = imgUrl; + break; + case ImageType.Background: + show.ImgBackdrop = imgUrl; + break; + default: + break; + } + } + } +} \ No newline at end of file diff --git a/Kyoo.sln b/Kyoo.sln index d11b3041..12f6f2ad 100644 --- a/Kyoo.sln +++ b/Kyoo.sln @@ -1,6 +1,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kyoo", "Kyoo\Kyoo.csproj", "{0F8275B6-C7DD-42DF-A168-755C81B1C329}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Common", "Kyoo.Common\Kyoo.Common.csproj", "{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -11,5 +13,9 @@ Global {0F8275B6-C7DD-42DF-A168-755C81B1C329}.Debug|Any CPU.Build.0 = Debug|Any CPU {0F8275B6-C7DD-42DF-A168-755C81B1C329}.Release|Any CPU.ActiveCfg = Release|Any CPU {0F8275B6-C7DD-42DF-A168-755C81B1C329}.Release|Any CPU.Build.0 = Release|Any CPU + {BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/Kyoo.sln.DotSettings b/Kyoo.sln.DotSettings index c1483371..f0549f01 100644 --- a/Kyoo.sln.DotSettings +++ b/Kyoo.sln.DotSettings @@ -4,4 +4,8 @@ UseExplicitType UseExplicitType API - DB \ No newline at end of file + DB + True + True + True + True \ No newline at end of file diff --git a/Kyoo/Controllers/Crawler.cs b/Kyoo/Controllers/Crawler.cs new file mode 100644 index 00000000..38f1ad2a --- /dev/null +++ b/Kyoo/Controllers/Crawler.cs @@ -0,0 +1,269 @@ +using System; +using Kyoo.Models; +using Microsoft.Extensions.Configuration; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using Kyoo.Models.Watch; + +namespace Kyoo.Controllers +{ + public class Crawler : ICrawler + { + private bool isRunning; + private readonly CancellationTokenSource cancellation; + + private readonly ILibraryManager libraryManager; + private readonly IProviderManager metadataProvider; + private readonly ITranscoder transcoder; + private readonly IConfiguration config; + + public Crawler(ILibraryManager libraryManager, IProviderManager metadataProvider, ITranscoder transcoder, IConfiguration configuration) + { + this.libraryManager = libraryManager; + this.metadataProvider = metadataProvider; + this.transcoder = transcoder; + config = configuration; + cancellation = new CancellationTokenSource(); + } + + public void Start() + { + if (isRunning) + return; + isRunning = true; + StartAsync(cancellation.Token); + } + + public void Cancel() + { + if (!isRunning) + return; + isRunning = false; + cancellation.Cancel(); + } + + private async void StartAsync(CancellationToken cancellationToken) + { + IEnumerable episodes = libraryManager.GetAllEpisodes(); + IEnumerable libraries = libraryManager.GetLibraries(); + + Debug.WriteLine("&Crawler started"); + foreach (Episode episode in episodes) + { + if (!File.Exists(episode.Path)) + libraryManager.RemoveEpisode(episode); + } + + foreach (Library library in libraries) + await Scan(library, cancellationToken); + + isRunning = false; + Debug.WriteLine("&Crawler stopped"); + } + + private async Task Scan(Library library, CancellationToken cancellationToken) + { + Console.WriteLine($"Scanning library {library.Name} at {string.Concat(library.Paths)}"); + foreach (string path in library.Paths) + { + foreach (string file in Directory.GetFiles(path, "*", SearchOption.AllDirectories)) + { + if (cancellationToken.IsCancellationRequested) + return; + if (!IsVideo(file)) + continue; + string relativePath = file.Substring(path.Length); + await RegisterFile(file, relativePath, library); + } + } + } + + private async Task RegisterFile(string path, string relativePath, Library library) + { + if (!libraryManager.IsEpisodeRegistered(path)) + { + string patern = config.GetValue("regex"); + Regex regex = new Regex(patern, RegexOptions.IgnoreCase); + Match match = regex.Match(relativePath); + + string showPath = Path.GetDirectoryName(path); + string collectionName = match.Groups["Collection"]?.Value; + string showName = match.Groups["ShowTitle"].Value; + bool seasonSuccess = long.TryParse(match.Groups["Season"].Value, out long seasonNumber); + bool episodeSucess = long.TryParse(match.Groups["Episode"].Value, out long episodeNumber); + long absoluteNumber = -1; + + Console.WriteLine("&Registering episode at: " + path); + if (!seasonSuccess || !episodeSucess) + { + //Considering that the episode is using absolute path. + seasonNumber = -1; + episodeNumber = -1; + + regex = new Regex(config.GetValue("absoluteRegex")); + match = regex.Match(relativePath); + + showName = match.Groups["ShowTitle"].Value; + bool absoluteSucess = long.TryParse(match.Groups["AbsoluteNumber"].Value, out absoluteNumber); + + if (!absoluteSucess) + { + Console.WriteLine("&Couldn't find basic data for the episode (regexs didn't match) " + relativePath); + return; + } + } + + Show show = await RegisterOrGetShow(collectionName, showName, showPath, library); + if (show != null) + await RegisterEpisode(show, seasonNumber, episodeNumber, absoluteNumber, path, library); + else + Console.Error.WriteLine($"Coudld not get informations about the show ${showName}."); + } + } + + private async Task RegisterOrGetShow(string collectionName, string showTitle, string showPath, Library library) + { + string showProviderIDs; + + if (!libraryManager.IsShowRegistered(showPath, out long showID)) + { + Show show = await metadataProvider.GetShowFromName(showTitle, library); + show.Path = showPath; + show.Title = show.Title ?? showTitle; + show.Slug = show.Slug ?? Utility.ToSlug(showTitle); + showProviderIDs = show.ExternalIDs; + showID = libraryManager.RegisterShow(show); + + if (showID == -1) + return null; + + libraryManager.RegisterInLibrary(showID, library); + if (!string.IsNullOrEmpty(collectionName)) + { + if (!libraryManager.IsCollectionRegistered(Utility.ToSlug(collectionName), out long collectionID)) + { + Collection collection = await metadataProvider.GetCollectionFromName(collectionName, library); + collection.Name = collection.Name ?? collectionName; + collectionID = libraryManager.RegisterCollection(collection); + } + libraryManager.AddShowToCollection(showID, collectionID); + } + + IEnumerable actors = await metadataProvider.GetPeople(show, library); + libraryManager.RegisterShowPeople(showID, actors); + } + else + showProviderIDs = libraryManager.GetShowExternalIDs(showID); + + return new Show { ID = showID, ExternalIDs = showProviderIDs, Title = showTitle }; + } + + private async Task RegisterSeason(Show show, long seasonNumber, Library library) + { + if (!libraryManager.IsSeasonRegistered(show.ID, seasonNumber, out long seasonID)) + { + Season season = await metadataProvider.GetSeason(show, seasonNumber, library); + season.ShowID = show.ID; + season.SeasonNumber = season.SeasonNumber == -1 ? seasonNumber : season.SeasonNumber; + season.Title ??= $"Season {season.SeasonNumber}"; + seasonID = libraryManager.RegisterSeason(season); + } + + return seasonID; + } + + private async Task RegisterEpisode(Show show, long seasonNumber, long episodeNumber, long absoluteNumber, string episodePath, Library library) + { + long seasonID = -1; + if (seasonNumber != -1) + seasonID = await RegisterSeason(show, seasonNumber, library); + + Episode episode = await metadataProvider.GetEpisode(show, seasonNumber, episodeNumber, absoluteNumber, library); + episode.ShowID = show.ID; + episode.Path = episodePath; + episode.SeasonNumber = episode.SeasonNumber != -1 ? episode.SeasonNumber : seasonNumber; + episode.EpisodeNumber = episode.EpisodeNumber != -1 ? episode.EpisodeNumber : episodeNumber; + episode.AbsoluteNumber = episode.AbsoluteNumber != -1 ? episode.AbsoluteNumber : absoluteNumber; + + if (seasonID == -1) + seasonID = await RegisterSeason(show, seasonNumber, library); + episode.SeasonID = seasonID; + episode.ID = libraryManager.RegisterEpisode(episode); + + Track[] tracks = await transcoder.GetTrackInfo(episode.Path); + int subcount = 0; + foreach (Track track in tracks) + { + if (track.Type == StreamType.Subtitle) + { + subcount++; + continue; + } + track.EpisodeID = episode.ID; + libraryManager.RegisterTrack(track); + } + + if (episode.Path.EndsWith(".mkv") && CountExtractedSubtitles(episode) != subcount) + { + Track[] subtitles = await transcoder.ExtractSubtitles(episode.Path); + if (subtitles != null) + { + foreach (Track track in subtitles) + { + track.EpisodeID = episode.ID; + libraryManager.RegisterTrack(track); + } + } + } + } + + private int CountExtractedSubtitles(Episode episode) + { + string path = Path.Combine(Path.GetDirectoryName(episode.Path), "Subtitles"); + int subcount = 0; + + if (!Directory.Exists(path)) + return 0; + foreach (string sub in Directory.EnumerateFiles(path, "", SearchOption.AllDirectories)) + { + string episodeLink = Path.GetFileNameWithoutExtension(episode.Path); + + if (!sub.Contains(episodeLink)) + continue; + string language = sub.Substring(Path.GetDirectoryName(sub).Length + episodeLink.Length + 2, 3); + bool isDefault = sub.Contains("default"); + bool isForced = sub.Contains("forced"); + Track track = new Track(StreamType.Subtitle, null, language, isDefault, isForced, null, false, sub) { EpisodeID = episode.ID }; + + if (Path.GetExtension(sub) == ".ass") + track.Codec = "ass"; + else if (Path.GetExtension(sub) == ".srt") + track.Codec = "subrip"; + else + track.Codec = null; + libraryManager.RegisterTrack(track); + subcount++; + } + return subcount; + } + + private static readonly string[] VideoExtensions = { ".webm", ".mkv", ".flv", ".vob", ".ogg", ".ogv", ".avi", ".mts", ".m2ts", ".ts", ".mov", ".qt", ".asf", ".mp4", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".3gp", ".3g2" }; + + private static bool IsVideo(string filePath) + { + return VideoExtensions.Contains(Path.GetExtension(filePath)); + } + + + public Task StopAsync() + { + cancellation.Cancel(); + return null; + } + } +} diff --git a/Kyoo/InternalAPI/LibraryManager/LibraryManager.cs b/Kyoo/Controllers/LibraryManager.cs similarity index 96% rename from Kyoo/InternalAPI/LibraryManager/LibraryManager.cs rename to Kyoo/Controllers/LibraryManager.cs index 69d9c630..0c3003ca 100644 --- a/Kyoo/InternalAPI/LibraryManager/LibraryManager.cs +++ b/Kyoo/Controllers/LibraryManager.cs @@ -7,7 +7,7 @@ using System.Data.SQLite; using System.Diagnostics; using System.IO; -namespace Kyoo.InternalAPI +namespace Kyoo.Controllers { public class LibraryManager : ILibraryManager { @@ -18,10 +18,9 @@ namespace Kyoo.InternalAPI { string databasePath = configuration.GetValue("databasePath"); - Debug.WriteLine("&Library Manager init, databasePath: " + databasePath); if (!File.Exists(databasePath)) { - Debug.WriteLine("&Database doesn't exist, creating one."); + Console.WriteLine($"Creating the database at {databasePath}."); if (!Directory.Exists(Path.GetDirectoryName(databasePath))) Directory.CreateDirectory(databasePath); @@ -29,7 +28,7 @@ namespace Kyoo.InternalAPI sqlConnection = new SQLiteConnection($"Data Source={databasePath};Version=3"); sqlConnection.Open(); - string createStatement = @"CREATE TABLE shows( + const string createStatement = @"CREATE TABLE shows( id INTEGER PRIMARY KEY UNIQUE, slug TEXT UNIQUE, title TEXT, @@ -92,7 +91,8 @@ namespace Kyoo.InternalAPI id INTEGER PRIMARY KEY UNIQUE, slug TEXT UNIQUE, name TEXT, - path TEXT + path TEXT, + providers TEXT ); CREATE TABLE librariesLinks( libraryID INTEGER, @@ -209,10 +209,11 @@ namespace Kyoo.InternalAPI public string GetShowExternalIDs(long showID) { - string query = string.Format("SELECT * FROM shows WHERE id = {0};", showID); - + string query = "SELECT * FROM shows WHERE id = $showID;"; + using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) { + cmd.Parameters.AddWithValue("$showID", showID); SQLiteDataReader reader = cmd.ExecuteReader(); if (reader.Read()) @@ -835,7 +836,7 @@ namespace Kyoo.InternalAPI Genre existingGenre = GetGenreBySlug(genre.Slug); if (existingGenre != null) - return existingGenre.id; + return existingGenre.ID; string query = "INSERT INTO genres (slug, name) VALUES($slug, $name);"; using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) @@ -864,7 +865,7 @@ namespace Kyoo.InternalAPI Studio existingStudio = GetStudioBySlug(studio.Slug); if (existingStudio != null) - return existingStudio.id; + return existingStudio.ID; string query = "INSERT INTO studios (slug, name) VALUES($slug, $name);"; using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) @@ -890,20 +891,20 @@ namespace Kyoo.InternalAPI public long GetOrCreatePeople(People people) { - People existingPeople = GetPeopleBySlug(people.slug); + People existingPeople = GetPeopleBySlug(people.Slug); if (existingPeople != null) - return existingPeople.id; + return existingPeople.ID; string query = "INSERT INTO people (slug, name, imgPrimary, externalIDs) VALUES($slug, $name, $imgPrimary, $externalIDs);"; using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) { try { - cmd.Parameters.AddWithValue("$slug", people.slug); + cmd.Parameters.AddWithValue("$slug", people.Slug); cmd.Parameters.AddWithValue("$name", people.Name); - cmd.Parameters.AddWithValue("$imgPrimary", people.imgPrimary); - cmd.Parameters.AddWithValue("$externalIDs", people.externalIDs); + cmd.Parameters.AddWithValue("$imgPrimary", people.ImgPrimary); + cmd.Parameters.AddWithValue("$externalIDs", people.ExternalIDs); cmd.ExecuteNonQuery(); cmd.CommandText = "SELECT LAST_INSERT_ROWID()"; @@ -913,7 +914,7 @@ namespace Kyoo.InternalAPI { Console.Error.WriteLine("SQL error while trying to insert a people ({0}).", people.Name); cmd.CommandText = "SELECT * FROM people WHERE slug = $slug"; - cmd.Parameters.AddWithValue("$slug", people.slug); + cmd.Parameters.AddWithValue("$slug", people.Slug); return (long)cmd.ExecuteScalar(); } @@ -949,13 +950,14 @@ namespace Kyoo.InternalAPI } } - public void RegisterInLibrary(long showID, string libraryPath) + public void RegisterInLibrary(long showID, Library library) { - string query = "INSERT INTO librariesLinks (libraryID, showID) SELECT id, $showID FROM libraries WHERE libraries.path = $libraryPath;"; + string query = + "INSERT INTO librariesLinks (libraryID, showID) SELECT id, $showID FROM libraries WHERE libraries.id = $libraryID;"; using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) { - cmd.Parameters.AddWithValue("$libraryPath", libraryPath); + cmd.Parameters.AddWithValue("$libraryID", library.ID); cmd.Parameters.AddWithValue("$showID", showID); cmd.ExecuteNonQuery(); } @@ -999,10 +1001,10 @@ namespace Kyoo.InternalAPI } } - if (show.studio != null) + if (show.Studio != null) { cmd.CommandText = "INSERT INTO studiosLinks (studioID, showID) VALUES($studioID, $showID);"; - long studioID = GetOrCreateStudio(show.studio); + long studioID = GetOrCreateStudio(show.Studio); cmd.Parameters.AddWithValue("$studioID", studioID); cmd.Parameters.AddWithValue("$showID", showID); cmd.ExecuteNonQuery(); @@ -1026,10 +1028,10 @@ namespace Kyoo.InternalAPI try { cmd.Parameters.AddWithValue("$showID", season.ShowID); - cmd.Parameters.AddWithValue("$seasonNumber", season.seasonNumber); + cmd.Parameters.AddWithValue("$seasonNumber", season.SeasonNumber); cmd.Parameters.AddWithValue("$title", season.Title); cmd.Parameters.AddWithValue("$overview", season.Overview); - cmd.Parameters.AddWithValue("$year", season.year); + cmd.Parameters.AddWithValue("$year", season.Year); cmd.Parameters.AddWithValue("$imgPrimary", season.ImgPrimary); cmd.Parameters.AddWithValue("$externalIDs", season.ExternalIDs); cmd.ExecuteNonQuery(); @@ -1042,7 +1044,7 @@ namespace Kyoo.InternalAPI Console.Error.WriteLine("SQL error while trying to insert a season ({0}), season probably already registered.", season.Title); cmd.CommandText = "SELECT * FROM seasons WHERE showID = $showID AND seasonNumber = $seasonNumber"; cmd.Parameters.AddWithValue("$showID", season.ShowID); - cmd.Parameters.AddWithValue("$seasonNumber", season.seasonNumber); + cmd.Parameters.AddWithValue("$seasonNumber", season.SeasonNumber); return (long)cmd.ExecuteScalar(); } } @@ -1057,9 +1059,9 @@ namespace Kyoo.InternalAPI { cmd.Parameters.AddWithValue("$showID", episode.ShowID); cmd.Parameters.AddWithValue("$seasonID", episode.SeasonID); - cmd.Parameters.AddWithValue("$seasonNUmber", episode.seasonNumber); - cmd.Parameters.AddWithValue("$episodeNumber", episode.episodeNumber); - cmd.Parameters.AddWithValue("$absoluteNumber", episode.absoluteNumber); + cmd.Parameters.AddWithValue("$seasonNUmber", episode.SeasonNumber); + cmd.Parameters.AddWithValue("$episodeNumber", episode.EpisodeNumber); + cmd.Parameters.AddWithValue("$absoluteNumber", episode.AbsoluteNumber); cmd.Parameters.AddWithValue("$path", episode.Path); cmd.Parameters.AddWithValue("$title", episode.Title); cmd.Parameters.AddWithValue("$overview", episode.Overview); @@ -1077,8 +1079,8 @@ namespace Kyoo.InternalAPI Console.Error.WriteLine("SQL error while trying to insert an episode ({0}), episode probably already registered.", episode.Link); cmd.CommandText = "SELECT * FROM episodes WHERE showID = $showID AND seasonNumber = $seasonNumber AND episodeNumber = $episodeNumber"; cmd.Parameters.AddWithValue("$showID", episode.ShowID); - cmd.Parameters.AddWithValue("$seasonNumber", episode.seasonNumber); - cmd.Parameters.AddWithValue("$episodeNumber", episode.episodeNumber); + cmd.Parameters.AddWithValue("$seasonNumber", episode.SeasonNumber); + cmd.Parameters.AddWithValue("$episodeNumber", episode.EpisodeNumber); return (long)cmd.ExecuteScalar(); } } @@ -1089,7 +1091,7 @@ namespace Kyoo.InternalAPI string query = "INSERT INTO tracks (episodeID, streamType, title, language, codec, isDefault, isForced, isExternal, path) VALUES($episodeID, $streamType, $title, $language, $codec, $isDefault, $isForced, $isExternal, $path);"; using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) { - cmd.Parameters.AddWithValue("$episodeID", track.episodeID); + cmd.Parameters.AddWithValue("$episodeID", track.EpisodeID); cmd.Parameters.AddWithValue("$streamType", track.Type); cmd.Parameters.AddWithValue("$title", track.Title); cmd.Parameters.AddWithValue("$language", track.Language); @@ -1102,21 +1104,21 @@ namespace Kyoo.InternalAPI } } - public void RegisterShowPeople(long showID, List people) + public void RegisterShowPeople(long showID, IEnumerable people) { if (people == null) return; string linkQuery = "INSERT INTO peopleLinks (peopleID, showID, role, type) VALUES($peopleID, $showID, $role, $type);"; - for (int i = 0; i < people.Count; i++) + foreach (People peop in people) { using (SQLiteCommand cmd = new SQLiteCommand(linkQuery, sqlConnection)) { - cmd.Parameters.AddWithValue("$peopleID", GetOrCreatePeople(people[i])); + cmd.Parameters.AddWithValue("$peopleID", GetOrCreatePeople(peop)); cmd.Parameters.AddWithValue("$showID", showID); - cmd.Parameters.AddWithValue("$role", people[i].Role); - cmd.Parameters.AddWithValue("$type", people[i].Type); + cmd.Parameters.AddWithValue("$role", peop.Role); + cmd.Parameters.AddWithValue("$type", peop.Type); cmd.ExecuteNonQuery(); } } @@ -1164,11 +1166,11 @@ namespace Kyoo.InternalAPI using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) { - cmd.Parameters.AddWithValue("$episodeID", episode.id); + cmd.Parameters.AddWithValue("$episodeID", episode.ID); cmd.ExecuteNonQuery(); } - if (GetEpisodes(episode.ShowID, episode.seasonNumber).Count == 0) + if (GetEpisodes(episode.ShowID, episode.SeasonNumber).Count == 0) RemoveSeason(episode.ShowID, episode.SeasonID); } diff --git a/Kyoo/Controllers/PluginManager.cs b/Kyoo/Controllers/PluginManager.cs new file mode 100644 index 00000000..3c41fa0e --- /dev/null +++ b/Kyoo/Controllers/PluginManager.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Kyoo.Models; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Kyoo.Controllers +{ + public class PluginManager : IPluginManager + { + private readonly IServiceProvider provider; + private readonly IConfiguration config; + private List plugins; + + public PluginManager(IServiceProvider provider, IConfiguration config) + { + this.provider = provider; + this.config = config; + } + + public T GetPlugin(string name) + { + if (plugins == null) + return default; + return (T)(from plugin in plugins where plugin.Name == name && plugin is T + select plugin).FirstOrDefault(); + } + + public IEnumerable GetPlugins() + { + if (plugins == null) + return null; + return from plugin in plugins where plugin is T + select (T)plugin; + } + + public void ReloadPlugins() + { + string pluginFolder = config.GetValue("plugins"); + + if (!Directory.Exists(pluginFolder)) + return; + string[] pluginsPaths = Directory.GetFiles(pluginFolder); + + plugins = pluginsPaths.Select(path => + { + try + { + Assembly ass = Assembly.LoadFile(Path.GetFullPath(path)); + return (from type in ass.GetTypes() + where typeof(IPlugin).IsAssignableFrom(type) + select (IPlugin) ActivatorUtilities.CreateInstance(provider, type)).FirstOrDefault(); + } + catch (Exception ex) + { + Console.Error.WriteLine($"Error loading the plugin at {path}.\nException: {ex.Message}"); + return null; + } + }).Where(x => x != null).ToList(); + } + } +} \ No newline at end of file diff --git a/Kyoo/Controllers/ProviderManager.cs b/Kyoo/Controllers/ProviderManager.cs new file mode 100644 index 00000000..bf743f50 --- /dev/null +++ b/Kyoo/Controllers/ProviderManager.cs @@ -0,0 +1,86 @@ +using System; +using Kyoo.Models; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Kyoo.Controllers.ThumbnailsManager; + +namespace Kyoo.Controllers +{ + public class ProviderManager : IProviderManager + { + private readonly IEnumerable providers; + private readonly IThumbnailsManager thumbnailsManager; + + public ProviderManager(IThumbnailsManager thumbnailsManager, IPluginManager pluginManager) + { + this.thumbnailsManager = thumbnailsManager; + providers = pluginManager.GetPlugins(); + } + + public async Task GetMetadata(Func> providerCall, Library library, string what) where T : IMergable, new() + { + T ret = new T(); + + foreach (IMetadataProvider provider in providers.OrderBy(provider => Array.IndexOf(library.Providers, provider.Name))) + { + try + { + if (library.Providers.Contains(provider.Name)) + ret = ret.Merge(await providerCall(provider)); + } catch (Exception ex) { + Console.Error.WriteLine($"The provider {provider.Name} coudln't work for {what}. (Exception: {ex.Message}"); + } + } + return ret; + } + + public async Task> GetMetadata(Func>> providerCall, Library library, string what) + { + List ret = new List(); + + foreach (IMetadataProvider provider in providers.OrderBy(provider => Array.IndexOf(library.Providers, provider.Name))) + { + try + { + if (library.Providers.Contains(provider.Name)) + ret.AddRange(await providerCall(provider)); + } catch (Exception ex) { + Console.Error.WriteLine($"The provider {provider.Name} coudln't work for {what}. (Excepetion: {ex.Message}"); + } + } + return ret; + } + + public async Task GetCollectionFromName(string name, Library library) + { + return await GetMetadata(provider => provider.GetCollectionFromName(name), library, $"the collection {name}"); + } + + public async Task GetShowFromName(string showName, Library library) + { + Show show = await GetMetadata(provider => provider.GetShowFromName(showName), library, $"the show {showName}"); + await thumbnailsManager.Validate(show); + return show; + } + + public async Task GetSeason(Show show, long seasonNumber, Library library) + { + return await GetMetadata(provider => provider.GetSeason(show, seasonNumber), library, $"the season {seasonNumber} of {show.Title}"); + } + + public async Task GetEpisode(Show show, long seasonNumber, long episodeNumber, long absoluteNumber, Library library) + { + Episode episode = await GetMetadata(provider => provider.GetEpisode(show, seasonNumber, episodeNumber, absoluteNumber), library, "an episode"); + await thumbnailsManager.Validate(episode); + return episode; + } + + public async Task> GetPeople(Show show, Library library) + { + IEnumerable people = await GetMetadata(provider => provider.GetPeople(show), library, "unknown data"); + people = await thumbnailsManager.Validate(people); + return people; + } + } +} diff --git a/Kyoo/InternalAPI/ThumbnailsManager/ThumbnailsManager.cs b/Kyoo/Controllers/ThumbnailsManager.cs similarity index 72% rename from Kyoo/InternalAPI/ThumbnailsManager/ThumbnailsManager.cs rename to Kyoo/Controllers/ThumbnailsManager.cs index 94b11fd3..fed87f8a 100644 --- a/Kyoo/InternalAPI/ThumbnailsManager/ThumbnailsManager.cs +++ b/Kyoo/Controllers/ThumbnailsManager.cs @@ -7,7 +7,7 @@ using System.IO; using System.Net; using System.Threading.Tasks; -namespace Kyoo.InternalAPI.ThumbnailsManager +namespace Kyoo.Controllers.ThumbnailsManager { public class ThumbnailsManager : IThumbnailsManager { @@ -20,6 +20,8 @@ namespace Kyoo.InternalAPI.ThumbnailsManager public async Task Validate(Show show) { + if (show == null || show.Path == null) + return null; string localThumb = Path.Combine(show.Path, "poster.jpg"); string localLogo = Path.Combine(show.Path, "logo.png"); string localBackdrop = Path.Combine(show.Path, "backdrop.jpg"); @@ -67,25 +69,26 @@ namespace Kyoo.InternalAPI.ThumbnailsManager return show; } - public async Task> Validate(List people) + public async Task> Validate(IEnumerable people) { - for (int i = 0; i < people?.Count; i++) + if (people == null) + return null; + foreach (People peop in people) { string root = config.GetValue("peoplePath"); Directory.CreateDirectory(root); - string localThumb = root + "/" + people[i].slug + ".jpg"; - if (people[i].imgPrimary != null && !File.Exists(localThumb)) + string localThumb = root + "/" + peop.Slug + ".jpg"; + if (peop.ImgPrimary == null || File.Exists(localThumb)) + continue; + try { - try - { - using WebClient client = new WebClient(); - await client.DownloadFileTaskAsync(new Uri(people[i].imgPrimary), localThumb); - } - catch (WebException) - { - Console.Error.WriteLine("Couldn't download an image."); - } + using WebClient client = new WebClient(); + await client.DownloadFileTaskAsync(new Uri(peop.ImgPrimary), localThumb); + } + catch (WebException) + { + Console.Error.WriteLine("Couldn't download an image."); } } @@ -94,18 +97,19 @@ namespace Kyoo.InternalAPI.ThumbnailsManager public async Task Validate(Episode episode) { - string localThumb = Path.ChangeExtension(episode.Path, "jpg"); - if (episode.ImgPrimary != null && !File.Exists(localThumb)) + if (episode == null || episode.Path == null) + return null; + string localThumb = Path.ChangeExtension(episode.Path, "jpg"); + if (episode.ImgPrimary == null || File.Exists(localThumb)) + return episode; + try { - try - { - using WebClient client = new WebClient(); - await client.DownloadFileTaskAsync(new Uri(episode.ImgPrimary), localThumb); - } - catch (WebException) - { - Console.Error.WriteLine("Couldn't download an image."); - } + using WebClient client = new WebClient(); + await client.DownloadFileTaskAsync(new Uri(episode.ImgPrimary), localThumb); + } + catch (WebException) + { + Console.Error.WriteLine("Couldn't download an image."); } return episode; diff --git a/Kyoo/InternalAPI/Transcoder/Transcoder.cs b/Kyoo/Controllers/Transcoder/Transcoder.cs similarity index 98% rename from Kyoo/InternalAPI/Transcoder/Transcoder.cs rename to Kyoo/Controllers/Transcoder/Transcoder.cs index cae60986..47811e4f 100644 --- a/Kyoo/InternalAPI/Transcoder/Transcoder.cs +++ b/Kyoo/Controllers/Transcoder/Transcoder.cs @@ -1,14 +1,15 @@ using System; using Kyoo.Models; -using Kyoo.InternalAPI.TranscoderLink; using Microsoft.Extensions.Configuration; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.Threading.Tasks; +using Kyoo.Controllers.TranscoderLink; + #pragma warning disable 4014 -namespace Kyoo.InternalAPI +namespace Kyoo.Controllers { public class Transcoder : ITranscoder { diff --git a/Kyoo/InternalAPI/Transcoder/TranscoderAPI.cs b/Kyoo/Controllers/Transcoder/TranscoderAPI.cs similarity index 98% rename from Kyoo/InternalAPI/Transcoder/TranscoderAPI.cs rename to Kyoo/Controllers/Transcoder/TranscoderAPI.cs index 27c13f31..b95c0584 100644 --- a/Kyoo/InternalAPI/Transcoder/TranscoderAPI.cs +++ b/Kyoo/Controllers/Transcoder/TranscoderAPI.cs @@ -5,7 +5,7 @@ using Kyoo.Models; using Kyoo.Models.Watch; // ReSharper disable InconsistentNaming -namespace Kyoo.InternalAPI.TranscoderLink +namespace Kyoo.Controllers.TranscoderLink { public static class TranscoderAPI { diff --git a/Kyoo/Controllers/AdminController.cs b/Kyoo/HtmlAPI/AdminController.cs similarity index 76% rename from Kyoo/Controllers/AdminController.cs rename to Kyoo/HtmlAPI/AdminController.cs index 976f9140..2ac4ce86 100644 --- a/Kyoo/Controllers/AdminController.cs +++ b/Kyoo/HtmlAPI/AdminController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; @@ -18,10 +18,10 @@ namespace Kyoo.Controllers this.crawler = crawler; } - [HttpGet("scan/{watch}")] - public IActionResult ScanLibrary(bool watch) + [HttpGet("scan")] + public IActionResult ScanLibrary() { - crawler.Start(watch); + crawler.Start(); return Ok("Scanning"); } } diff --git a/Kyoo/Controllers/CollectionController.cs b/Kyoo/HtmlAPI/CollectionController.cs similarity index 96% rename from Kyoo/Controllers/CollectionController.cs rename to Kyoo/HtmlAPI/CollectionController.cs index 9e1a84e1..8d1a8543 100644 --- a/Kyoo/Controllers/CollectionController.cs +++ b/Kyoo/HtmlAPI/CollectionController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; diff --git a/Kyoo/Controllers/EpisodesController.cs b/Kyoo/HtmlAPI/EpisodesController.cs similarity index 97% rename from Kyoo/Controllers/EpisodesController.cs rename to Kyoo/HtmlAPI/EpisodesController.cs index ac2ac30e..b3876234 100644 --- a/Kyoo/Controllers/EpisodesController.cs +++ b/Kyoo/HtmlAPI/EpisodesController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; diff --git a/Kyoo/Controllers/LibrariesController.cs b/Kyoo/HtmlAPI/LibrariesController.cs similarity index 90% rename from Kyoo/Controllers/LibrariesController.cs rename to Kyoo/HtmlAPI/LibrariesController.cs index a2c05df3..4c4faee0 100644 --- a/Kyoo/Controllers/LibrariesController.cs +++ b/Kyoo/HtmlAPI/LibrariesController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; @@ -30,7 +30,7 @@ namespace Kyoo.Controllers if (library == null) return NotFound(); - return libraryManager.GetShowsInLibrary(library.id); + return libraryManager.GetShowsInLibrary(library.ID); } } } \ No newline at end of file diff --git a/Kyoo/Controllers/PeopleController.cs b/Kyoo/HtmlAPI/PeopleController.cs similarity index 78% rename from Kyoo/Controllers/PeopleController.cs rename to Kyoo/HtmlAPI/PeopleController.cs index 9c08379f..bd133892 100644 --- a/Kyoo/Controllers/PeopleController.cs +++ b/Kyoo/HtmlAPI/PeopleController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Diagnostics; @@ -23,10 +23,10 @@ namespace Kyoo.Controllers if (people == null) return NotFound(); - Collection collection = new Collection(0, people.slug, people.Name, null, null) + Collection collection = new Collection(0, people.Slug, people.Name, null, null) { - Shows = libraryManager.GetShowsByPeople(people.id), - Poster = "peopleimg/" + people.slug + Shows = libraryManager.GetShowsByPeople(people.ID), + Poster = "peopleimg/" + people.Slug }; return collection; } diff --git a/Kyoo/Controllers/SearchController.cs b/Kyoo/HtmlAPI/SearchController.cs similarity index 62% rename from Kyoo/Controllers/SearchController.cs rename to Kyoo/HtmlAPI/SearchController.cs index 1dd08d17..6a33330f 100644 --- a/Kyoo/Controllers/SearchController.cs +++ b/Kyoo/HtmlAPI/SearchController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; @@ -20,12 +20,12 @@ namespace Kyoo.Controllers { SearchResult result = new SearchResult { - query = query, - shows = libraryManager.GetShows(query), - episodes = libraryManager.SearchEpisodes(query), - people = libraryManager.SearchPeople(query), - genres = libraryManager.SearchGenres(query), - studios = libraryManager.SearchStudios(query) + Query = query, + Shows = libraryManager.GetShows(query), + Episodes = libraryManager.SearchEpisodes(query), + People = libraryManager.SearchPeople(query), + Genres = libraryManager.SearchGenres(query), + Studios = libraryManager.SearchStudios(query) }; return result; } diff --git a/Kyoo/Controllers/ShowsController.cs b/Kyoo/HtmlAPI/ShowsController.cs similarity index 96% rename from Kyoo/Controllers/ShowsController.cs rename to Kyoo/HtmlAPI/ShowsController.cs index 97f7ecae..436f6c45 100644 --- a/Kyoo/Controllers/ShowsController.cs +++ b/Kyoo/HtmlAPI/ShowsController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; diff --git a/Kyoo/Controllers/SubtitleController.cs b/Kyoo/HtmlAPI/SubtitleController.cs similarity index 95% rename from Kyoo/Controllers/SubtitleController.cs rename to Kyoo/HtmlAPI/SubtitleController.cs index c3487a42..178c354c 100644 --- a/Kyoo/Controllers/SubtitleController.cs +++ b/Kyoo/HtmlAPI/SubtitleController.cs @@ -1,8 +1,6 @@ -using Kyoo.InternalAPI; -using Kyoo.Models; +using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Threading.Tasks; @@ -52,12 +50,12 @@ namespace Kyoo.Controllers public async Task ExtractSubtitle(string showSlug, long seasonNumber, long episodeNumber) { Episode episode = libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); - libraryManager.ClearSubtitles(episode.id); + libraryManager.ClearSubtitles(episode.ID); Track[] tracks = await transcoder.ExtractSubtitles(episode.Path); foreach (Track track in tracks) { - track.episodeID = episode.id; + track.EpisodeID = episode.ID; libraryManager.RegisterTrack(track); } @@ -70,12 +68,12 @@ namespace Kyoo.Controllers List episodes = libraryManager.GetEpisodes(showSlug); foreach (Episode episode in episodes) { - libraryManager.ClearSubtitles(episode.id); + libraryManager.ClearSubtitles(episode.ID); Track[] tracks = await transcoder.ExtractSubtitles(episode.Path); foreach (Track track in tracks) { - track.episodeID = episode.id; + track.EpisodeID = episode.ID; libraryManager.RegisterTrack(track); } } diff --git a/Kyoo/Controllers/ThumbnailController.cs b/Kyoo/HtmlAPI/ThumbnailController.cs similarity index 96% rename from Kyoo/Controllers/ThumbnailController.cs rename to Kyoo/HtmlAPI/ThumbnailController.cs index 74100b7a..6c0bd2ba 100644 --- a/Kyoo/Controllers/ThumbnailController.cs +++ b/Kyoo/HtmlAPI/ThumbnailController.cs @@ -1,11 +1,10 @@ -using Kyoo.InternalAPI; -using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; using System.IO; namespace Kyoo.Controllers { - public class ThumbnailController : Controller + public class ThumbnailController : ControllerBase { private readonly ILibraryManager libraryManager; private readonly string peoplePath; diff --git a/Kyoo/Controllers/VideoController.cs b/Kyoo/HtmlAPI/VideoController.cs similarity index 99% rename from Kyoo/Controllers/VideoController.cs rename to Kyoo/HtmlAPI/VideoController.cs index c5ebc43c..b58da90f 100644 --- a/Kyoo/Controllers/VideoController.cs +++ b/Kyoo/HtmlAPI/VideoController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; diff --git a/Kyoo/Controllers/WatchController.cs b/Kyoo/HtmlAPI/WatchController.cs similarity index 97% rename from Kyoo/Controllers/WatchController.cs rename to Kyoo/HtmlAPI/WatchController.cs index 0de4d726..a1eadf95 100644 --- a/Kyoo/Controllers/WatchController.cs +++ b/Kyoo/HtmlAPI/WatchController.cs @@ -1,4 +1,4 @@ -using Kyoo.InternalAPI; +using Kyoo.Controllers; using Kyoo.Models; using Microsoft.AspNetCore.Mvc; using System.Diagnostics; diff --git a/Kyoo/InternalAPI/Crawler/Crawler.cs b/Kyoo/InternalAPI/Crawler/Crawler.cs deleted file mode 100644 index 89ff7daf..00000000 --- a/Kyoo/InternalAPI/Crawler/Crawler.cs +++ /dev/null @@ -1,328 +0,0 @@ -using Kyoo.InternalAPI.Utility; -using Kyoo.Models; -using Microsoft.Extensions.Configuration; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; -using Kyoo.Models.Watch; - -namespace Kyoo.InternalAPI -{ - public class Crawler : ICrawler - { - private static ICrawler runningCrawler; - private bool isScanning; - private readonly CancellationTokenSource cancellation; - - private readonly ILibraryManager libraryManager; - private readonly IMetadataProvider metadataProvider; - private readonly ITranscoder transcoder; - private readonly IConfiguration config; - - public Crawler(ILibraryManager libraryManager, IMetadataProvider metadataProvider, ITranscoder transcoder, IConfiguration configuration) - { - this.libraryManager = libraryManager; - this.metadataProvider = metadataProvider; - this.transcoder = transcoder; - config = configuration; - cancellation = new CancellationTokenSource(); - } - - public async Task Start(bool watch) - { - if (runningCrawler == null) - { - runningCrawler = this; - await StartAsync(watch, cancellation.Token); - } - else if (runningCrawler is Crawler crawler) - { - if (!crawler.isScanning) - { - await crawler.StopAsync(); - runningCrawler = this; - await StartAsync(watch, cancellation.Token); - } - } - } - - private Task StartAsync(bool watch, CancellationToken cancellationToken) - { - IEnumerable episodes = libraryManager.GetAllEpisodes(); - IEnumerable libraryPaths = libraryManager.GetLibrariesPath(); - - isScanning = true; - Debug.WriteLine("&Crawler started"); - foreach (Episode episode in episodes) - { - if (!File.Exists(episode.Path)) - libraryManager.RemoveEpisode(episode); - } - - foreach (string path in libraryPaths) - { - Scan(path, cancellationToken); - - if(watch) - Watch(path, cancellationToken); - } - - isScanning = false; - if (watch) - while (!cancellationToken.IsCancellationRequested); - Debug.WriteLine("&Crawler stopped"); - runningCrawler = null; - return null; - } - - private async void Scan(string folderPath, CancellationToken cancellationToken) - { - string[] files = Directory.GetFiles(folderPath, "*", SearchOption.AllDirectories); - - foreach (string file in files) - { - if (cancellationToken.IsCancellationRequested) - return; - - if (IsVideo(file)) - { - Debug.WriteLine("&Registering episode at: " + file); - await ExtractEpisodeData(file, folderPath); - } - } - } - - private void Watch(string folderPath, CancellationToken cancellationToken) - { - Debug.WriteLine("Folder watching not implemented yet."); - //Debug.WriteLine("&Watching " + folderPath + " for changes"); - //using (FileSystemWatcher watcher = new FileSystemWatcher()) - //{ - // watcher.Path = folderPath; - // watcher.IncludeSubdirectories = true; - // watcher.NotifyFilter = NotifyFilters.LastAccess - // | NotifyFilters.LastWrite - // | NotifyFilters.FileName - // | NotifyFilters.Size - // | NotifyFilters.DirectoryName; - - // watcher.Created += FileCreated; - // watcher.Changed += FileChanged; - // watcher.Renamed += FileRenamed; - // watcher.Deleted += FileDeleted; - - - // watcher.EnableRaisingEvents = true; - - // while (!cancellationToken.IsCancellationRequested); - //} - } - - //private void FileCreated(object sender, FileSystemEventArgs e) - //{ - // Debug.WriteLine("&File Created at " + e.FullPath); - // if (IsVideo(e.FullPath)) - // { - // Debug.WriteLine("&Created file is a video"); - // _ = TryRegisterEpisode(e.FullPath); - // } - //} - - //private void FileChanged(object sender, FileSystemEventArgs e) - //{ - // Debug.WriteLine("&File Changed at " + e.FullPath); - //} - - //private void FileRenamed(object sender, RenamedEventArgs e) - //{ - // Debug.WriteLine("&File Renamed at " + e.FullPath); - //} - - //private void FileDeleted(object sender, FileSystemEventArgs e) - //{ - // Debug.WriteLine("&File Deleted at " + e.FullPath); - //} - - private async Task ExtractEpisodeData(string episodePath, string libraryPath) - { - if (!libraryManager.IsEpisodeRegistered(episodePath)) - { - string relativePath = episodePath.Substring(libraryPath.Length); - string patern = config.GetValue("regex"); - Regex regex = new Regex(patern, RegexOptions.IgnoreCase); - Match match = regex.Match(relativePath); - - string showPath = Path.GetDirectoryName(episodePath); - string collectionName = match.Groups["Collection"]?.Value; - string showName = match.Groups["ShowTitle"].Value; - bool seasonSuccess = long.TryParse(match.Groups["Season"].Value, out long seasonNumber); - bool episodeSucess = long.TryParse(match.Groups["Episode"].Value, out long episodeNumber); - long absoluteNumber = -1; - - if (!seasonSuccess || !episodeSucess) - { - //Considering that the episode is using absolute path. - seasonNumber = -1; - episodeNumber = -1; - - regex = new Regex(config.GetValue("absoluteRegex")); - match = regex.Match(relativePath); - - showName = match.Groups["ShowTitle"].Value; - bool absoluteSucess = long.TryParse(match.Groups["AbsoluteNumber"].Value, out absoluteNumber); - - if (!absoluteSucess) - { - Debug.WriteLine("&Couldn't find basic data for the episode (regexs didn't match)" + relativePath); - return; - } - } - - Show show = await RegisterOrGetShow(collectionName, showName, showPath, libraryPath); - if (show != null) - await RegisterEpisode(show, seasonNumber, episodeNumber, absoluteNumber, episodePath); - } - } - - private async Task RegisterOrGetShow(string collectionName, string showTitle, string showPath, string libraryPath) - { - string showProviderIDs; - - if (!libraryManager.IsShowRegistered(showPath, out long showID)) - { - Show show = await metadataProvider.GetShowFromName(showTitle, showPath); - showProviderIDs = show.ExternalIDs; - showID = libraryManager.RegisterShow(show); - - if (showID == -1) - return null; - - libraryManager.RegisterInLibrary(showID, libraryPath); - if (!string.IsNullOrEmpty(collectionName)) - { - if (!libraryManager.IsCollectionRegistered(Slugifier.ToSlug(collectionName), out long collectionID)) - { - Collection collection = await metadataProvider.GetCollectionFromName(collectionName); - collectionID = libraryManager.RegisterCollection(collection); - } - libraryManager.AddShowToCollection(showID, collectionID); - } - - List actors = await metadataProvider.GetPeople(show.ExternalIDs); - libraryManager.RegisterShowPeople(showID, actors); - } - else - showProviderIDs = libraryManager.GetShowExternalIDs(showID); - - return new Show { id = showID, ExternalIDs = showProviderIDs, Title = showTitle }; - } - - private async Task RegisterEpisode(Show show, long seasonNumber, long episodeNumber, long absoluteNumber, string episodePath) - { - long seasonID = -1; - if (seasonNumber != -1) - { - if (!libraryManager.IsSeasonRegistered(show.id, seasonNumber, out seasonID)) - { - Season season = await metadataProvider.GetSeason(show.Title, seasonNumber); - season.ShowID = show.id; - seasonID = libraryManager.RegisterSeason(season); - } - } - - Episode episode = await metadataProvider.GetEpisode(show.ExternalIDs, seasonNumber, episodeNumber, absoluteNumber, episodePath); - episode.ShowID = show.id; - - if (seasonID == -1) - { - if (!libraryManager.IsSeasonRegistered(show.id, episode.seasonNumber, out seasonID)) - { - Season season = await metadataProvider.GetSeason(show.Title, episode.seasonNumber); - season.ShowID = show.id; - seasonID = libraryManager.RegisterSeason(season); - } - } - - episode.SeasonID = seasonID; - episode.id = libraryManager.RegisterEpisode(episode); - - Track[] tracks = await transcoder.GetTrackInfo(episode.Path); - int subcount = 0; - foreach (Track track in tracks) - { - if (track.Type == StreamType.Subtitle) - { - subcount++; - continue; - } - track.episodeID = episode.id; - libraryManager.RegisterTrack(track); - } - - if (episode.Path.EndsWith(".mkv")) - { - if (CountExtractedSubtitles(episode) != subcount) - { - Track[] subtitles = await transcoder.ExtractSubtitles(episode.Path); - if (subtitles != null) - { - foreach (Track track in subtitles) - { - track.episodeID = episode.id; - libraryManager.RegisterTrack(track); - } - } - } - } - } - - private int CountExtractedSubtitles(Episode episode) - { - string path = Path.Combine(Path.GetDirectoryName(episode.Path), "Subtitles"); - int subcount = 0; - - if (!Directory.Exists(path)) - return 0; - foreach (string sub in Directory.EnumerateFiles(path, "", SearchOption.AllDirectories)) - { - string episodeLink = Path.GetFileNameWithoutExtension(episode.Path); - - if (sub.Contains(episodeLink)) - { - string language = sub.Substring(Path.GetDirectoryName(sub).Length + episodeLink.Length + 2, 3); - bool isDefault = sub.Contains("default"); - bool isForced = sub.Contains("forced"); - Track track = new Track(StreamType.Subtitle, null, language, isDefault, isForced, null, false, sub) { episodeID = episode.id }; - - if (Path.GetExtension(sub) == ".ass") - track.Codec = "ass"; - else if (Path.GetExtension(sub) == ".srt") - track.Codec = "subrip"; - else - track.Codec = null; - libraryManager.RegisterTrack(track); - subcount++; - } - } - return subcount; - } - - private static readonly string[] VideoExtensions = { ".webm", ".mkv", ".flv", ".vob", ".ogg", ".ogv", ".avi", ".mts", ".m2ts", ".ts", ".mov", ".qt", ".asf", ".mp4", ".m4p", ".m4v", ".mpg", ".mp2", ".mpeg", ".mpe", ".mpv", ".m2v", ".3gp", ".3g2" }; - - private static bool IsVideo(string filePath) - { - return VideoExtensions.Contains(Path.GetExtension(filePath)); - } - - - public Task StopAsync() - { - cancellation.Cancel(); - return null; - } - } -} diff --git a/Kyoo/InternalAPI/MetadataProvider/IMetadataProvider.cs b/Kyoo/InternalAPI/MetadataProvider/IMetadataProvider.cs deleted file mode 100644 index 1a53a4b5..00000000 --- a/Kyoo/InternalAPI/MetadataProvider/IMetadataProvider.cs +++ /dev/null @@ -1,25 +0,0 @@ -using Kyoo.Models; -using System.Collections.Generic; -using System.Threading.Tasks; - -namespace Kyoo.InternalAPI -{ - public interface IMetadataProvider - { - //For the collection - Task GetCollectionFromName(string name); - - //For the show - Task GetShowByID(string id); - Task GetShowFromName(string showName, string showPath); - Task GetImages(Show show); - Task> GetPeople(string id); - - //For the seasons - Task GetSeason(string showName, long seasonNumber); - Task GetSeasonImage(string showName, long seasonNumber); - - //For the episodes - Task GetEpisode(string externalIDs, long seasonNumber, long episodeNumber, long absoluteNumber, string episodePath); - } -} diff --git a/Kyoo/InternalAPI/MetadataProvider/Implementations/TheTvDB/HelperTvDB.cs b/Kyoo/InternalAPI/MetadataProvider/Implementations/TheTvDB/HelperTvDB.cs deleted file mode 100644 index 37678244..00000000 --- a/Kyoo/InternalAPI/MetadataProvider/Implementations/TheTvDB/HelperTvDB.cs +++ /dev/null @@ -1,68 +0,0 @@ -using Kyoo.Models; -using Newtonsoft.Json; -using System; -using System.Diagnostics; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; - -namespace Kyoo.InternalAPI.MetadataProvider.TheTvDB -{ - public class HelperTvDB : ProviderHelper - { - public override string Provider => "TvDB"; - - private string token; - private DateTime tokenDate; - - protected async Task Authentificate() - { - if (DateTime.Now.Subtract(tokenDate) < TimeSpan.FromDays(1)) - return token; - - HttpClient client = new HttpClient(); - HttpContent content = new StringContent("{ \"apikey\": \"IM2OXA8UHUIU0GH6\" }", Encoding.UTF8, "application/json"); - - try - { - HttpResponseMessage response = await client.PostAsync("https://api.thetvdb.com/login", content); - - if (response.StatusCode == HttpStatusCode.OK) - { - string resp = await response.Content.ReadAsStringAsync(); - var obj = new {Token = ""}; - - token = JsonConvert.DeserializeAnonymousType(resp, obj).Token; - tokenDate = DateTime.UtcNow; - return token; - } - Debug.WriteLine("&Couldn't authentificate in TheTvDB API.\nError status: " + response.StatusCode + " Message: " + response.RequestMessage); - } - catch (WebException ex) - { - Debug.WriteLine("&Couldn't authentificate in TheTvDB API.\nError status: " + ex.Status); - return null; - } - return null; - } - - - protected static long? GetYear(string firstAired) - { - if (firstAired?.Length >= 4 && long.TryParse(firstAired.Substring(0, 4), out long year)) - return year; - - return null; - } - - public Status? GetStatus(string status) - { - if (status == "Ended") - return Status.Finished; - if (status == "Continuing") - return Status.Airing; - return null; - } - } -} diff --git a/Kyoo/InternalAPI/MetadataProvider/Implementations/TheTvDB/ProviderTheTvDB.cs b/Kyoo/InternalAPI/MetadataProvider/Implementations/TheTvDB/ProviderTheTvDB.cs deleted file mode 100644 index d9b5d6e4..00000000 --- a/Kyoo/InternalAPI/MetadataProvider/Implementations/TheTvDB/ProviderTheTvDB.cs +++ /dev/null @@ -1,325 +0,0 @@ -using Kyoo.InternalAPI.MetadataProvider.TheTvDB; -using Kyoo.InternalAPI.Utility; -using Kyoo.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net; -using System.Threading.Tasks; -using System.Web; -#pragma warning disable 1998 - -namespace Kyoo.InternalAPI.MetadataProvider -{ - [MetaProvider] - public class ProviderTheTvDB : HelperTvDB, IMetadataProvider - { - public async Task GetCollectionFromName(string name) - { - return new Collection(-1, Slugifier.ToSlug(name), name, null, null); - } - - public async Task GetShowFromName(string showName, string showPath) - { - string token = await Authentificate(); - - if (token != null) - { - WebRequest request = WebRequest.Create("https://api.thetvdb.com/search/series?name=" + HttpUtility.UrlEncode(showName)); - request.Method = "GET"; - request.Timeout = 12000; - request.ContentType = "application/json"; - request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + token); - - try - { - HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync(); - - if (response.StatusCode == HttpStatusCode.OK) - { - Stream stream = response.GetResponseStream(); - if (stream != null) - { - using StreamReader reader = new StreamReader(stream); - string content = await reader.ReadToEndAsync(); - stream.Close(); - response.Close(); - - dynamic obj = JsonConvert.DeserializeObject(content); - dynamic data = obj.data[0]; - - Show show = new Show(-1, - ToSlug(showName), - (string) data.seriesName, - ((JArray) data.aliases).ToObject>(), - showPath, - (string) data.overview, - null, //trailer - null, //genres (no info with this request) - GetStatus((string) data.status), - GetYear((string) data.firstAired), - null, //endYear - string.Format("{0}={1}|", Provider, (string) data.id)); - return (await GetShowByID(GetID(show.ExternalIDs))).Set(show.Slug, show.Path) ?? show; - } - } - else - { - Debug.WriteLine("&TheTvDB Provider couldn't work for this show: " + showName + ".\nError Code: " + response.StatusCode + " Message: " + response.StatusDescription); - response.Close(); - } - } - catch (WebException ex) - { - Debug.WriteLine("&TheTvDB Provider couldn't work for this show: " + showName + ".\nError Code: " + ex.Status); - } - } - - return new Show() { Slug = ToSlug(showName), Title = showName, Path = showPath }; - } - - public async Task GetShowByID(string id) - { - string token = await Authentificate(); - - if (token == null) - return null; - - WebRequest request = WebRequest.Create("https://api.thetvdb.com/series/" + id); - request.Method = "GET"; - request.Timeout = 12000; - request.ContentType = "application/json"; - request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + token); - - try - { - HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync(); - - if (response.StatusCode == HttpStatusCode.OK) - { - Stream stream = response.GetResponseStream(); - if (stream != null) - { - using StreamReader reader = new StreamReader(stream); - string content = await reader.ReadToEndAsync(); - stream.Close(); - response.Close(); - - dynamic model = JsonConvert.DeserializeObject(content); - dynamic data = model.data; - - Show show = new Show(-1, - null, //Slug - (string) data.seriesName, - ((JArray) data.aliases).ToObject>(), - null, //Path - (string) data.overview, - null, //Trailer - GetGenres(((JArray) data.genre).ToObject()), - GetStatus((string) data.status), - GetYear((string) data.firstAired), - null, //endYear - $"TvDB={id}|"); - await GetImages(show); - return show; - } - } - Debug.WriteLine("&TheTvDB Provider couldn't work for the show with the id: " + id + ".\nError Code: " + response.StatusCode + " Message: " + response.StatusDescription); - response.Close(); - return null; - } - catch(WebException ex) - { - Debug.WriteLine("&TheTvDB Provider couldn't work for the show with the id: " + id + ".\nError Code: " + ex.Status); - return null; - } - } - - public async Task GetImages(Show show) - { - Debug.WriteLine("&Getting images for: " + show.Title); - string id = GetID(show.ExternalIDs); - - if (id == null) - return show; - - string token = await Authentificate(); - - if (token == null) - return show; - - Dictionary imageTypes = new Dictionary { { ImageType.Poster, "poster" }, { ImageType.Background, "fanart" } }; - - foreach (KeyValuePair type in imageTypes) - { - try - { - WebRequest request = WebRequest.Create("https://api.thetvdb.com/series/" + id + "/images/query?keyType=" + type.Value); - request.Method = "GET"; - request.Timeout = 12000; - request.ContentType = "application/json"; - request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + token); - - HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync(); - - if (response.StatusCode == HttpStatusCode.OK) - { - Stream stream = response.GetResponseStream(); - if (stream != null) - { - using StreamReader reader = new StreamReader(stream); - string content = await reader.ReadToEndAsync(); - stream.Close(); - response.Close(); - - dynamic model = JsonConvert.DeserializeObject(content); - //Should implement language selection here - dynamic data = ((IEnumerable) model.data) - .OrderByDescending(x => x.ratingsInfo.average) - .ThenByDescending(x => x.ratingsInfo.count).FirstOrDefault(); - if (data != null) - SetImage(show, "https://www.thetvdb.com/banners/" + data.fileName, type.Key); - } - } - else - { - Debug.WriteLine("&TheTvDB Provider couldn't get " + type + " for the show with the id: " + id + ".\nError Code: " + response.StatusCode + " Message: " + response.StatusDescription); - response.Close(); - } - } - catch (WebException ex) - { - Debug.WriteLine("&TheTvDB Provider couldn't get " + type + " for the show with the id: " + id + ".\nError Code: " + ex.Status); - } - } - - return show; - } - - public async Task GetSeason(string showName, long seasonNumber) - { - return new Season(-1, -1, seasonNumber, "Season " + seasonNumber, null, null, null, null); - } - - public Task GetSeasonImage(string showName, long seasonNumber) - { - return null; - } - - public async Task GetEpisode(string externalIDs, long seasonNumber, long episodeNumber, long absoluteNumber, string episodePath) - { - string id = GetID(externalIDs); - - if (id == null) - return new Episode(seasonNumber, episodeNumber, absoluteNumber, null, null, null, -1, null, externalIDs); - - string token = await Authentificate(); - - if (token == null) - return new Episode(seasonNumber, episodeNumber, absoluteNumber, null, null, null, -1, null, externalIDs); - - WebRequest request; - if (absoluteNumber != -1) - request = WebRequest.Create("https://api.thetvdb.com/series/" + id + "/episodes/query?absoluteNumber=" + absoluteNumber); - else - request = WebRequest.Create("https://api.thetvdb.com/series/" + id + "/episodes/query?airedSeason=" + seasonNumber + "&airedEpisode=" + episodeNumber); - - request.Method = "GET"; - request.Timeout = 12000; - request.ContentType = "application/json"; - request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + token); - - try - { - HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync(); - - if (response.StatusCode == HttpStatusCode.OK) - { - Stream stream = response.GetResponseStream(); - if (stream != null) - { - using StreamReader reader = new StreamReader(stream); - string content = await reader.ReadToEndAsync(); - stream.Close(); - response.Close(); - - dynamic data = JsonConvert.DeserializeObject(content); - dynamic episode = data.data[0]; - - DateTime dateTime = DateTime.ParseExact((string)episode.firstAired, "yyyy-MM-dd", CultureInfo.InvariantCulture); - - if (absoluteNumber == -1) - absoluteNumber = (long?)episode.absoluteNumber ?? -1; - else - { - seasonNumber = episode.airedSeason; - episodeNumber = episode.airedEpisodeNumber; - } - - return new Episode(seasonNumber, episodeNumber, absoluteNumber, (string)episode.episodeName, (string)episode.overview, dateTime, -1, "https://www.thetvdb.com/banners/" + episode.filename, string.Format("TvDB={0}|", episode.id)); - } - } - Debug.WriteLine("&TheTvDB Provider couldn't work for the episode number: " + episodeNumber + ".\nError Code: " + response.StatusCode + " Message: " + response.StatusDescription); - response.Close(); - return new Episode(seasonNumber, episodeNumber, absoluteNumber, null, null, null, -1, null, externalIDs); - } - catch (WebException ex) - { - Debug.WriteLine("&TheTvDB Provider couldn't work for the episode number: " + episodeNumber + ".\nError Code: " + ex.Status); - return new Episode(seasonNumber, episodeNumber, absoluteNumber, null, null, null, -1, null, externalIDs); - } - } - - public async Task> GetPeople(string externalIDs) - { - string id = GetID(externalIDs); - - if (id == null) - return null; - - string token = await Authentificate(); - - if (token == null) - return null; - - WebRequest request = WebRequest.Create("https://api.thetvdb.com/series/" + id + "/actors"); - request.Method = "GET"; - request.Timeout = 12000; - request.ContentType = "application/json"; - request.Headers.Add(HttpRequestHeader.Authorization, "Bearer " + token); - - try - { - HttpWebResponse response = (HttpWebResponse)await request.GetResponseAsync(); - - if (response.StatusCode == HttpStatusCode.OK) - { - Stream stream = response.GetResponseStream(); - if (stream != null) - { - using StreamReader reader = new StreamReader(stream); - string content = await reader.ReadToEndAsync(); - stream.Close(); - response.Close(); - - dynamic data = JsonConvert.DeserializeObject(content); - return (((IEnumerable)data.data).OrderBy(x => x.sortOrder)).ToList().ConvertAll(x => { return new People(-1, ToSlug((string)x.name), (string)x.name, (string)x.role, null, "https://www.thetvdb.com/banners/" + (string)x.image, string.Format("TvDB={0}|", x.id)); }); - } - } - Debug.WriteLine("&TheTvDB Provider couldn't work for the actors of the show: " + id + ".\nError Code: " + response.StatusCode + " Message: " + response.StatusDescription); - response.Close(); - return null; - } - catch (WebException ex) - { - Debug.WriteLine("&TheTvDB Provider couldn't work for the actors of the show: " + id + ".\nError Code: " + ex.Status); - return null; - } - } - } -} diff --git a/Kyoo/InternalAPI/MetadataProvider/MetadataAttribute.cs b/Kyoo/InternalAPI/MetadataProvider/MetadataAttribute.cs deleted file mode 100644 index cd72a719..00000000 --- a/Kyoo/InternalAPI/MetadataProvider/MetadataAttribute.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; - -namespace Kyoo.InternalAPI.MetadataProvider -{ - [AttributeUsage(AttributeTargets.Class)] - public class MetaProvider : Attribute - { - public MetaProvider() - { - - } - } -} diff --git a/Kyoo/InternalAPI/MetadataProvider/ProviderHelper.cs b/Kyoo/InternalAPI/MetadataProvider/ProviderHelper.cs deleted file mode 100644 index 82e5220e..00000000 --- a/Kyoo/InternalAPI/MetadataProvider/ProviderHelper.cs +++ /dev/null @@ -1,60 +0,0 @@ -using Kyoo.InternalAPI.Utility; -using Kyoo.Models; -using System.Collections.Generic; - -namespace Kyoo.InternalAPI.MetadataProvider -{ - public abstract class ProviderHelper - { - public abstract string Provider { get; } - - public string GetID(string externalIDs) - { - if (externalIDs?.Contains(Provider) == true) - { - int startIndex = externalIDs.IndexOf(Provider) + Provider.Length + 1; //The + 1 is for the '=' - return externalIDs.Substring(startIndex, externalIDs.IndexOf('|', startIndex) - startIndex); - } - else - return null; - } - - public string ToSlug(string showTitle) - { - return Slugifier.ToSlug(showTitle); - } - - public enum ImageType { Poster, Background, Thumbnail, Logo } - - public void SetImage(Show show, string imgUrl, ImageType type) - { - switch(type) - { - case ImageType.Poster: - show.ImgPrimary = imgUrl; - break; - case ImageType.Thumbnail: - show.ImgThumb = imgUrl; - break; - case ImageType.Logo: - show.ImgLogo = imgUrl; - break; - case ImageType.Background: - show.ImgBackdrop = imgUrl; - break; - default: - break; - } - } - - public IEnumerable GetGenres(string[] input) - { - List genres = new List(); - - foreach (string genre in input) - genres.Add(new Genre(ToSlug(genre), genre)); - - return genres; - } - } -} diff --git a/Kyoo/InternalAPI/MetadataProvider/ProviderManager.cs b/Kyoo/InternalAPI/MetadataProvider/ProviderManager.cs deleted file mode 100644 index 4847ebed..00000000 --- a/Kyoo/InternalAPI/MetadataProvider/ProviderManager.cs +++ /dev/null @@ -1,163 +0,0 @@ -using Kyoo.InternalAPI.MetadataProvider; -using Kyoo.InternalAPI.ThumbnailsManager; -using Kyoo.Models; -using Microsoft.Extensions.Configuration; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading.Tasks; - -namespace Kyoo.InternalAPI -{ - public class ProviderManager : IMetadataProvider - { - private readonly List providers = new List(); - private readonly IThumbnailsManager thumbnailsManager; - private readonly IConfiguration config; - - public ProviderManager(IThumbnailsManager thumbnailsManager, IConfiguration configuration) - { - this.thumbnailsManager = thumbnailsManager; - config = configuration; - LoadProviders(); - } - - void LoadProviders() - { - providers.Clear(); - providers.Add(new ProviderTheTvDB()); - - string pluginFolder = config.GetValue("providerPlugins"); - - if (Directory.Exists(pluginFolder)) - { - string[] pluginsPaths = Directory.GetFiles(pluginFolder); - List plugins = new List(); - List types = new List(); - - for (int i = 0; i < pluginsPaths.Length; i++) - { - plugins.Add(Assembly.LoadFile(pluginsPaths[i])); - types.AddRange(plugins[i].GetTypes()); - } - - List providersPlugins = types.FindAll(x => - { - object[] atr = x.GetCustomAttributes(typeof(MetaProvider), false); - - if (atr == null || atr.Length == 0) - return false; - - List interfaces = new List(x.GetInterfaces()); - - if (interfaces.Contains(typeof(IMetadataProvider))) - return true; - - return false; - }); - - providers.AddRange(providersPlugins.ConvertAll(x => Activator.CreateInstance(x) as IMetadataProvider)); - } - } - - public Show Merge(IEnumerable shows) - { - return shows.FirstOrDefault(); - } - - public Season Merge(IEnumerable seasons) - { - return seasons.FirstOrDefault(); - } - - public Episode Merge(IEnumerable episodes) - { - return episodes.FirstOrDefault(); //Should do something if the return is null; - } - - //For all the following methods, it should use all providers and merge the data. - - public Task GetCollectionFromName(string name) - { - return providers[0].GetCollectionFromName(name); - } - - public Task GetImages(Show show) - { - return providers[0].GetImages(show); - } - - public async Task GetSeason(string showName, int seasonNumber) - { - List datas = new List(); - for (int i = 0; i < providers.Count; i++) - { - datas.Add(await providers[i].GetSeason(showName, seasonNumber)); - } - - return Merge(datas); - } - - public async Task GetShowByID(string id) - { - List datas = new List(); - for (int i = 0; i < providers.Count; i++) - { - datas.Add(await providers[i].GetShowByID(id)); - } - - return Merge(datas); - } - - public async Task GetShowFromName(string showName, string showPath) - { - List datas = new List(); - for (int i = 0; i < providers.Count; i++) - { - datas.Add(await providers[i].GetShowFromName(showName, showPath)); - } - - Show show = Merge(datas); - return await thumbnailsManager.Validate(show); - } - - public async Task GetSeason(string showName, long seasonNumber) - { - List datas = new List(); - for (int i = 0; i < providers.Count; i++) - { - datas.Add(await providers[i].GetSeason(showName, seasonNumber)); - } - - return Merge(datas); - } - - public Task GetSeasonImage(string showName, long seasonNumber) - { - //Should select the best provider for this show. - - return providers[0].GetSeasonImage(showName, seasonNumber); - } - - public async Task GetEpisode(string externalIDs, long seasonNumber, long episodeNumber, long absoluteNumber, string episodePath) - { - List datas = new List(); - for (int i = 0; i < providers.Count; i++) - { - datas.Add(await providers[i].GetEpisode(externalIDs, seasonNumber, episodeNumber, absoluteNumber, episodePath)); - } - - Episode episode = Merge(datas); - episode.Path = episodePath; - return await thumbnailsManager.Validate(episode); - } - - public async Task> GetPeople(string id) - { - List actors = await providers[0].GetPeople(id); - return await thumbnailsManager.Validate(actors); - } - } -} diff --git a/Kyoo/InternalAPI/Utility/Slugifier.cs b/Kyoo/InternalAPI/Utility/Slugifier.cs deleted file mode 100644 index 395c4d77..00000000 --- a/Kyoo/InternalAPI/Utility/Slugifier.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Text.RegularExpressions; - -namespace Kyoo.InternalAPI.Utility -{ - public class Slugifier - { - public static string ToSlug(string showTitle) - { - if (showTitle == null) - return null; - - //First to lower case - showTitle = showTitle.ToLowerInvariant(); - - //Remove all accents - //var bytes = Encoding.GetEncoding("Cyrillic").GetBytes(showTitle); - //showTitle = Encoding.ASCII.GetString(bytes); - - //Replace spaces - showTitle = Regex.Replace(showTitle, @"\s", "-", RegexOptions.Compiled); - - //Remove invalid chars - showTitle = Regex.Replace(showTitle, @"[^\w\s\p{Pd}]", "", RegexOptions.Compiled); - - //Trim dashes from end - showTitle = showTitle.Trim('-', '_'); - - //Replace double occurences of - or \_ - showTitle = Regex.Replace(showTitle, @"([-_]){2,}", "$1", RegexOptions.Compiled); - - return showTitle; - } - } -} diff --git a/Kyoo/Kyoo.csproj b/Kyoo/Kyoo.csproj index 795b09a7..d5ffa11a 100644 --- a/Kyoo/Kyoo.csproj +++ b/Kyoo/Kyoo.csproj @@ -17,6 +17,7 @@ + @@ -29,6 +30,8 @@ + + @@ -44,6 +47,15 @@ Always + + + + + + + + + diff --git a/Kyoo/Kyoo.csproj.DotSettings b/Kyoo/Kyoo.csproj.DotSettings index 74a9e4de..bc646adf 100644 --- a/Kyoo/Kyoo.csproj.DotSettings +++ b/Kyoo/Kyoo.csproj.DotSettings @@ -1,4 +1,7 @@  + True + True + True True True True diff --git a/Kyoo/Models/Collection.cs b/Kyoo/Models/Collection.cs deleted file mode 100644 index 0ac1ad3b..00000000 --- a/Kyoo/Models/Collection.cs +++ /dev/null @@ -1,50 +0,0 @@ -using Kyoo.InternalAPI; -using Newtonsoft.Json; -using System.Collections.Generic; - -namespace Kyoo.Models -{ - public class Collection - { - [JsonIgnore] public long id; - public string Slug; - public string Name; - public string Poster; - public string Overview; - [JsonIgnore] public string ImgPrimary; - public IEnumerable Shows; - - public Collection() { } - - public Collection(long id, string slug, string name, string overview, string imgPrimary) - { - this.id = id; - Slug = slug; - Name = name; - Overview = overview; - ImgPrimary = imgPrimary; - } - - public static Collection FromReader(System.Data.SQLite.SQLiteDataReader reader) - { - Collection col = new Collection((long)reader["id"], - reader["slug"] as string, - reader["name"] as string, - reader["overview"] as string, - reader["imgPrimary"] as string); - col.Poster = "poster/" + col.Slug; - return col; - } - - public Show AsShow() - { - return new Show(-1, Slug, Name, null, null, Overview, null, null, null, null, null, null); - } - - public Collection SetShows(ILibraryManager libraryManager) - { - Shows = libraryManager.GetShowsInCollection(id); - return this; - } - } -} diff --git a/Kyoo/Models/SearchResult.cs b/Kyoo/Models/SearchResult.cs deleted file mode 100644 index 5462c904..00000000 --- a/Kyoo/Models/SearchResult.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System.Collections.Generic; - -namespace Kyoo.Models -{ - public class SearchResult - { - public string query; - public IEnumerable shows; - public IEnumerable episodes; - public IEnumerable people; - public IEnumerable genres; - public IEnumerable studios; - } -} diff --git a/Kyoo/Program.cs b/Kyoo/Program.cs index 06c7b1e2..925d7334 100644 --- a/Kyoo/Program.cs +++ b/Kyoo/Program.cs @@ -1,14 +1,27 @@ +using System.Threading.Tasks; +using Kyoo.Controllers; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; namespace Kyoo { public class Program { - public static void Main(string[] args) + public static async Task Main(string[] args) { - CreateWebHostBuilder(args).Build().Run(); + IWebHost host = CreateWebHostBuilder(args).Build(); + + using (IServiceScope serviceScope = host.Services.CreateScope()) + { + IPluginManager pluginManager = serviceScope.ServiceProvider.GetService(); + pluginManager.ReloadPlugins(); + + ICrawler crawler = serviceScope.ServiceProvider.GetService(); + crawler.Start(); + } + await host.RunAsync(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => diff --git a/Kyoo/Startup.cs b/Kyoo/Startup.cs index 71ea8672..b62bff68 100644 --- a/Kyoo/Startup.cs +++ b/Kyoo/Startup.cs @@ -1,5 +1,5 @@ -using Kyoo.InternalAPI; -using Kyoo.InternalAPI.ThumbnailsManager; +using Kyoo.Controllers; +using Kyoo.Controllers.ThumbnailsManager; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.SpaServices.AngularCli; @@ -30,15 +30,14 @@ namespace Kyoo }); services.AddControllers().AddNewtonsoftJson(); - - //Services needed in the private and in the public API + services.AddHttpClient(); + services.AddSingleton(); services.AddSingleton(); - - //Services used to get informations about files and register them - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. diff --git a/Kyoo/appsettings.json b/Kyoo/appsettings.json index af27c3fe..7cd9243d 100644 --- a/Kyoo/appsettings.json +++ b/Kyoo/appsettings.json @@ -12,7 +12,7 @@ "transmuxTempPath": "/tmp/cached/kyoo/transmux", "transcodeTempPath": "/tmp/cached/kyoo/transcode", "peoplePath": "/tmp/people", - "plugins": "/tmp/plugins", + "plugins": "plugins/", "regex": "^(\\/(?.+?))?\\/.*\\/(?.+?) S(?\\d+)E(?\\d+)", "absoluteRegex": ".*\\/(?.+?) (?\\d+)" } diff --git a/Unit Tests/Kyoo-InternalAPI/Library-Tests.cs b/Unit Tests/Kyoo-InternalAPI/Library-Tests.cs deleted file mode 100644 index b3c061ff..00000000 --- a/Unit Tests/Kyoo-InternalAPI/Library-Tests.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Kyoo.InternalAPI; -using Microsoft.Extensions.Configuration; -using NUnit.Framework; - -namespace UnitTests.Kyoo_InternalAPI -{ - public class LibraryTests - { - private IConfiguration config; - private ILibraryManager libraryManager; - - [SetUp] - public void Setup() - { - config = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - libraryManager = new LibraryManager(config); - } - } -} diff --git a/Unit Tests/Kyoo-InternalAPI/Thumbnails-Tests.cs b/Unit Tests/Kyoo-InternalAPI/Thumbnails-Tests.cs deleted file mode 100644 index 8229a107..00000000 --- a/Unit Tests/Kyoo-InternalAPI/Thumbnails-Tests.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Kyoo.InternalAPI; -using Kyoo.InternalAPI.ThumbnailsManager; -using Kyoo.Models; -using Microsoft.Extensions.Configuration; -using NUnit.Framework; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace UnitTests.Kyoo_InternalAPI -{ - public class ThumbnailsTests - { - private IConfiguration config; - - [SetUp] - public void Setup() - { - config = new ConfigurationBuilder() - .AddJsonFile("appsettings.json") - .Build(); - } - - [Test] - public async Task DownloadShowImages() - { - LibraryManager library = new LibraryManager(config); - ThumbnailsManager manager = new ThumbnailsManager(config); - Show show = library.GetShowBySlug(library.GetShows().FirstOrDefault().Slug); - Debug.WriteLine("&Show: " + show.Path); - string posterPath = Path.Combine(show.Path, "poster.jpg"); - File.Delete(posterPath); - - await manager.Validate(show); - long posterLength = new FileInfo(posterPath).Length; - Assert.IsTrue(posterLength > 0, "Poster size is zero for the tested show (" + posterPath + ")"); - } - } -} diff --git a/Unit Tests/Unit Tests.csproj b/Unit Tests/Unit Tests.csproj deleted file mode 100644 index 779d6470..00000000 --- a/Unit Tests/Unit Tests.csproj +++ /dev/null @@ -1,29 +0,0 @@ - - - - netcoreapp3.1 - UnitTests - - false - - UnitTests - - - - - - - - - - - - - - - - PreserveNewest - - - - diff --git a/Unit Tests/appsettings.json b/Unit Tests/appsettings.json deleted file mode 100644 index 6fb127c3..00000000 --- a/Unit Tests/appsettings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "server.urls": "http://0.0.0.0:5000", - "https_port": 44300, - "Logging": { - "LogLevel": { - "Default": "Warning" - } - }, - "AllowedHosts": "*", - - "databasePath": "C://Projects/database.db", - "tempPath": "C:\\\\Projects\\temp", - "peoplePath": "D:\\\\Videos\\People", - "plugins": "C:\\Projects\\Kyoo\\Debug", - "providerPlugins": "C://Projects/Plugins/Providers", - "regex": "^(\\\\(?.+?))?\\\\.*\\\\(?.+?) S(?\\d+)E(?\\d+)", - "absoluteRegex": ".*\\\\(?.+?) (?\\d+)" -} diff --git a/transcoder b/transcoder index d8f72ff8..bedf98f8 160000 --- a/transcoder +++ b/transcoder @@ -1 +1 @@ -Subproject commit d8f72ff87020f2d38a4552f6083ef0659bf1f2be +Subproject commit bedf98f85d2cf669a1d1c4af35271841133cfec2