Merge pull request #5 from AnonymusRaccoon/Dev

Adding a plugin manager and moving TheTVDB metadata provider to a plugin.
This commit is contained in:
Zoe Roux 2020-01-25 17:06:28 +01:00
commit d18000a9c8
63 changed files with 1280 additions and 1406 deletions

233
Kyoo.Common/.gitignore vendored Normal file
View File

@ -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/

View File

@ -1,12 +1,12 @@
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Kyoo.InternalAPI namespace Kyoo.Controllers
{ {
public interface ICrawler public interface ICrawler
{ {
Task Start(bool watch); void Start();
Task StopAsync(); void Cancel();
} }
} }

View File

@ -2,7 +2,7 @@
using Kyoo.Models.Watch; using Kyoo.Models.Watch;
using System.Collections.Generic; using System.Collections.Generic;
namespace Kyoo.InternalAPI namespace Kyoo.Controllers
{ {
public interface ILibraryManager public interface ILibraryManager
{ {
@ -63,9 +63,9 @@ namespace Kyoo.InternalAPI
long GetOrCreateGenre(Genre genre); long GetOrCreateGenre(Genre genre);
long GetOrCreateStudio(Studio studio); long GetOrCreateStudio(Studio studio);
void RegisterShowPeople(long showID, List<People> actors); void RegisterShowPeople(long showID, IEnumerable<People> actors);
void AddShowToCollection(long showID, long collectionID); void AddShowToCollection(long showID, long collectionID);
void RegisterInLibrary(long showID, string libraryPath); void RegisterInLibrary(long showID, Library library);
void RemoveEpisode(Episode episode); void RemoveEpisode(Episode episode);
void ClearSubtitles(long episodeID); void ClearSubtitles(long episodeID);

View File

@ -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<Collection> GetCollectionFromName(string name);
//For the show
Task<Show> GetShowByID(string id);
Task<Show> GetShowFromName(string showName);
Task<IEnumerable<People>> GetPeople(Show show);
//For the seasons
Task<Season> GetSeason(Show show, long seasonNumber);
//For the episodes
Task<Episode> GetEpisode(Show show, long seasonNumber, long episodeNumber, long absoluteNumber);
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Kyoo.Controllers
{
public interface IPluginManager
{
public T GetPlugin<T>(string name);
public IEnumerable<T> GetPlugins<T>();
public void ReloadPlugins();
}
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Kyoo.Models;
namespace Kyoo.Controllers
{
public interface IProviderManager
{
Task<Collection> GetCollectionFromName(string name, Library library);
Task<Show> GetShowFromName(string showName, Library library);
Task<Season> GetSeason(Show show, long seasonNumber, Library library);
Task<Episode> GetEpisode(Show show, long seasonNumber, long episodeNumber, long absoluteNumber, Library library);
Task<IEnumerable<People>> GetPeople(Show show, Library library);
}
}

View File

@ -2,12 +2,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Kyoo.InternalAPI.ThumbnailsManager namespace Kyoo.Controllers.ThumbnailsManager
{ {
public interface IThumbnailsManager public interface IThumbnailsManager
{ {
Task<Show> Validate(Show show); Task<Show> Validate(Show show);
Task<List<People>> Validate(List<People> actors); Task<IEnumerable<People>> Validate(IEnumerable<People> actors);
Task<Episode> Validate(Episode episode); Task<Episode> Validate(Episode episode);
} }
} }

View File

@ -2,7 +2,7 @@ using Kyoo.Models;
using Kyoo.Models.Watch; using Kyoo.Models.Watch;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Kyoo.InternalAPI namespace Kyoo.Controllers
{ {
public interface ITranscoder public interface ITranscoder
{ {

View File

@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Title>Kyoo.Common</Title>
<Authors>Anonymus Raccoon</Authors>
<Description>Base package to create plugins for Kyoo.</Description>
<PackageProjectUrl>https://github.com/AnonymusRaccoon/Kyoo</PackageProjectUrl>
<RepositoryUrl>https://github.com/AnonymusRaccoon/Kyoo</RepositoryUrl>
<Company>SDG</Company>
<PackageLicenseExpression>GPL-3.0-or-later</PackageLicenseExpression>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageVersion>1.0.6</PackageVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.112" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,75 @@
using Kyoo.Controllers;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Kyoo.Models
{
public class Collection : IMergable<Collection>
{
[JsonIgnore] public long ID = -1;
public string Slug;
public string Name;
public string Poster;
public string Overview;
[JsonIgnore] public string ImgPrimary;
public IEnumerable<Show> 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;
}
}
}

View File

@ -3,15 +3,15 @@ using System;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Episode public class Episode : IMergable<Episode>
{ {
[JsonIgnore] public long id; [JsonIgnore] public long ID;
[JsonIgnore] public long ShowID; [JsonIgnore] public long ShowID;
[JsonIgnore] public long SeasonID; [JsonIgnore] public long SeasonID;
public long seasonNumber; public long SeasonNumber;
public long episodeNumber; public long EpisodeNumber;
public long absoluteNumber; public long AbsoluteNumber;
[JsonIgnore] public string Path; [JsonIgnore] public string Path;
public string Title; public string Title;
public string Overview; public string Overview;
@ -27,16 +27,24 @@ namespace Kyoo.Models
public string Thumb; //Used in the API response only 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) 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; ShowID = -1;
SeasonID = -1; SeasonID = -1;
this.seasonNumber = seasonNumber; SeasonNumber = seasonNumber;
this.episodeNumber = episodeNumber; EpisodeNumber = episodeNumber;
this.absoluteNumber = absoluteNumber; AbsoluteNumber = absoluteNumber;
Title = title; Title = title;
Overview = overview; Overview = overview;
ReleaseDate = releaseDate; 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) 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; ShowID = showID;
SeasonID = seasonID; SeasonID = seasonID;
this.seasonNumber = seasonNumber; SeasonNumber = seasonNumber;
this.episodeNumber = episodeNumber; EpisodeNumber = episodeNumber;
this.absoluteNumber = absoluteNumber; AbsoluteNumber = absoluteNumber;
Path = path; Path = path;
Title = title; Title = title;
Overview = overview; Overview = overview;
@ -82,7 +90,7 @@ namespace Kyoo.Models
public Episode SetThumb(string showSlug) public Episode SetThumb(string showSlug)
{ {
Link = GetSlug(showSlug, seasonNumber, episodeNumber); Link = GetSlug(showSlug, SeasonNumber, EpisodeNumber);
Thumb = "thumb/" + Link; Thumb = "thumb/" + Link;
return this; return this;
} }
@ -97,5 +105,37 @@ namespace Kyoo.Models
{ {
return showSlug + "-s" + seasonNumber + "e" + episodeNumber; 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;
}
} }
} }

View File

@ -4,7 +4,7 @@ namespace Kyoo.Models
{ {
public class Genre public class Genre
{ {
[JsonIgnore] public readonly long id; [JsonIgnore] public readonly long ID;
public string Slug; public string Slug;
public string Name; public string Name;
@ -16,7 +16,7 @@ namespace Kyoo.Models
public Genre(long id, string slug, string name) public Genre(long id, string slug, string name)
{ {
this.id = id; ID = id;
Slug = slug; Slug = slug;
Name = name; Name = name;
} }

View File

@ -0,0 +1,4 @@
namespace Kyoo.Models
{
public enum ImageType { Poster, Background, Thumbnail, Logo }
}

View File

@ -4,17 +4,19 @@ namespace Kyoo.Models
{ {
public class Library public class Library
{ {
[JsonIgnore] public readonly long id; [JsonIgnore] public readonly long ID;
public string Slug; public string Slug;
public string Name; 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; Slug = slug;
Name = name; Name = name;
Path = path; Paths = paths;
Providers = providers;
} }
public static Library FromReader(System.Data.SQLite.SQLiteDataReader reader) public static Library FromReader(System.Data.SQLite.SQLiteDataReader reader)
@ -22,7 +24,8 @@ namespace Kyoo.Models
return new Library((long)reader["id"], return new Library((long)reader["id"],
reader["slug"] as string, reader["slug"] as string,
reader["name"] as string, reader["name"] as string,
reader["path"] as string); (reader["path"] as string)?.Split('|'),
(reader["providers"] as string)?.Split('|'));
} }
} }
} }

View File

@ -2,35 +2,37 @@
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class People public class People : IMergable<People>
{ {
[JsonIgnore] public long id; [JsonIgnore] public long ID = -1;
public string slug; public string Slug;
public string Name; public string Name;
public string Role; //Dynamic data not stored as it in the database 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 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) public People(long id, string slug, string name, string imgPrimary, string externalIDs)
{ {
this.id = id; ID = id;
this.slug = slug; Slug = slug;
Name = name; Name = name;
this.imgPrimary = imgPrimary; ImgPrimary = imgPrimary;
this.externalIDs = externalIDs; ExternalIDs = externalIDs;
} }
public People(long id, string slug, string name, string role, string type, string imgPrimary, string externalIDs) public People(long id, string slug, string name, string role, string type, string imgPrimary, string externalIDs)
{ {
this.id = id; ID = id;
this.slug = slug; Slug = slug;
Name = name; Name = name;
Role = role; Role = role;
Type = type; Type = type;
this.imgPrimary = imgPrimary; ImgPrimary = imgPrimary;
this.externalIDs = externalIDs; ExternalIDs = externalIDs;
} }
public static People FromReader(System.Data.SQLite.SQLiteDataReader reader) public static People FromReader(System.Data.SQLite.SQLiteDataReader reader)
@ -52,5 +54,25 @@ namespace Kyoo.Models
reader["imgPrimary"] as string, reader["imgPrimary"] as string,
reader["externalIDs"] 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;
}
} }
} }

View File

@ -0,0 +1,7 @@
namespace Kyoo.Models
{
public interface IPlugin
{
public string Name { get; }
}
}

View File

@ -0,0 +1,14 @@
using System.Collections.Generic;
namespace Kyoo.Models
{
public class SearchResult
{
public string Query;
public IEnumerable<Show> Shows;
public IEnumerable<Episode> Episodes;
public IEnumerable<People> People;
public IEnumerable<Genre> Genres;
public IEnumerable<Studio> Studios;
}
}

View File

@ -2,15 +2,15 @@
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Season public class Season : IMergable<Season>
{ {
[JsonIgnore] public readonly long id; [JsonIgnore] public readonly long ID = -1;
[JsonIgnore] public long ShowID; [JsonIgnore] public long ShowID = -1;
public long seasonNumber; public long SeasonNumber = -1;
public string Title; public string Title;
public string Overview; public string Overview;
public long? year; public long? Year;
[JsonIgnore] public string ImgPrimary; [JsonIgnore] public string ImgPrimary;
public string ExternalIDs; 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) 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; ShowID = showID;
this.seasonNumber = seasonNumber; SeasonNumber = seasonNumber;
Title = title; Title = title;
Overview = overview; Overview = overview;
this.year = year; Year = year;
ImgPrimary = imgPrimary; ImgPrimary = imgPrimary;
ExternalIDs = externalIDs; ExternalIDs = externalIDs;
} }
@ -40,5 +40,25 @@ namespace Kyoo.Models
reader["imgPrimary"] as string, reader["imgPrimary"] as string,
reader["externalIDs"] 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;
}
} }
} }

View File

@ -1,12 +1,14 @@
using Kyoo.InternalAPI; using System;
using Kyoo.Controllers;
using Newtonsoft.Json; using Newtonsoft.Json;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Kyoo.Models namespace Kyoo.Models
{ {
public class Show public class Show : IMergable<Show>
{ {
[JsonIgnore] public long id = -1; [JsonIgnore] public long ID = -1;
public string Slug; public string Slug;
public string Title; public string Title;
@ -28,27 +30,21 @@ namespace Kyoo.Models
public string ExternalIDs; public string ExternalIDs;
//Used in the rest API excusively. //Used in the rest API excusively.
public Studio studio; public Studio Studio;
public IEnumerable<People> directors; public IEnumerable<People> Directors;
public IEnumerable<People> people; public IEnumerable<People> People;
public IEnumerable<Season> seasons; public IEnumerable<Season> Seasons;
public bool IsCollection; public bool IsCollection;
public string GetAliases() public string GetAliases()
{ {
if (Aliases == null) return Aliases == null ? null : string.Join('|', Aliases);
return null;
return string.Join('|', Aliases);
} }
public string GetGenres() public string GetGenres()
{ {
if (Genres == null) return Genres == null ? null : string.Join('|', Genres);
return null;
return string.Join('|', Genres);
} }
@ -56,7 +52,7 @@ namespace Kyoo.Models
public Show(long id, string slug, string title, IEnumerable<string> aliases, string path, string overview, string trailerUrl, IEnumerable<Genre> genres, Status? status, long? startYear, long? endYear, string externalIDs) public Show(long id, string slug, string title, IEnumerable<string> aliases, string path, string overview, string trailerUrl, IEnumerable<Genre> genres, Status? status, long? startYear, long? endYear, string externalIDs)
{ {
this.id = id; ID = id;
Slug = slug; Slug = slug;
Title = title; Title = title;
Aliases = aliases; Aliases = aliases;
@ -73,7 +69,7 @@ namespace Kyoo.Models
public Show(long id, string slug, string title, IEnumerable<string> aliases, string path, string overview, string trailerUrl, Status? status, long? startYear, long? endYear, string imgPrimary, string imgThumb, string imgLogo, string imgBackdrop, string externalIDs) public Show(long id, string slug, string title, IEnumerable<string> 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; Slug = slug;
Title = title; Title = title;
Aliases = aliases; Aliases = aliases;
@ -111,7 +107,7 @@ namespace Kyoo.Models
return new Show((long)reader["id"], return new Show((long)reader["id"],
reader["slug"] as string, reader["slug"] as string,
reader["title"] as string, reader["title"] as string,
(reader["aliases"] as string)?.Split('|') ?? null, (reader["aliases"] as string)?.Split('|'),
reader["path"] as string, reader["path"] as string,
reader["overview"] as string, reader["overview"] as string,
reader["trailerUrl"] as string, reader["trailerUrl"] as string,
@ -125,6 +121,16 @@ namespace Kyoo.Models
reader["externalIDs"] as string); 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) public Show Set(string slug, string path)
{ {
Slug = slug; Slug = slug;
@ -134,31 +140,73 @@ namespace Kyoo.Models
public Show SetGenres(ILibraryManager manager) public Show SetGenres(ILibraryManager manager)
{ {
Genres = manager.GetGenreForShow(id); Genres = manager.GetGenreForShow(ID);
return this; return this;
} }
public Show SetStudio(ILibraryManager manager) public Show SetStudio(ILibraryManager manager)
{ {
studio = manager.GetStudio(id); Studio = manager.GetStudio(ID);
return this; return this;
} }
public Show SetDirectors(ILibraryManager manager) public Show SetDirectors(ILibraryManager manager)
{ {
directors = manager.GetDirectors(id); Directors = manager.GetDirectors(ID);
return this; return this;
} }
public Show SetPeople(ILibraryManager manager) public Show SetPeople(ILibraryManager manager)
{ {
people = manager.GetPeople(id); People = manager.GetPeople(ID);
return this; return this;
} }
public Show SetSeasons(ILibraryManager manager) 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; return this;
} }
} }

View File

@ -4,7 +4,7 @@ namespace Kyoo.Models
{ {
public class Studio public class Studio
{ {
[JsonIgnore] public readonly long id; [JsonIgnore] public readonly long ID = -1;
public string Slug; public string Slug;
public string Name; public string Name;
@ -16,7 +16,7 @@ namespace Kyoo.Models
public Studio(long id, string slug, string name) public Studio(long id, string slug, string name)
{ {
this.id = id; ID = id;
Slug = slug; Slug = slug;
Name = name; Name = name;
} }

View File

@ -58,7 +58,7 @@ namespace Kyoo.Models
public string DisplayName; public string DisplayName;
public string Link; public string Link;
[JsonIgnore] public long episodeID; [JsonIgnore] public long EpisodeID;
[JsonIgnore] public bool IsExternal; [JsonIgnore] public bool IsExternal;
public Track(StreamType type, string title, string language, bool isDefault, bool isForced, string codec, bool isExternal, string path) 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") if (language == "fre")
language = "fra"; 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; Link = "/subtitle/" + episodeSlug + "." + Language;
if (IsForced) if (IsForced)

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Newtonsoft.Json; using Newtonsoft.Json;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -7,33 +7,33 @@ namespace Kyoo.Models
{ {
public class WatchItem public class WatchItem
{ {
[JsonIgnore] public readonly long episodeID; [JsonIgnore] public readonly long EpisodeID = -1;
public string ShowTitle; public string ShowTitle;
public string ShowSlug; public string ShowSlug;
public long seasonNumber; public long SeasonNumber;
public long episodeNumber; public long EpisodeNumber;
public string Title; public string Title;
public string Link; public string Link;
public DateTime? ReleaseDate; public DateTime? ReleaseDate;
[JsonIgnore] public string Path; [JsonIgnore] public string Path;
public string previousEpisode; public string PreviousEpisode;
public Episode nextEpisode; public Episode NextEpisode;
public string container; public string Container;
public Track video; public Track Video;
public IEnumerable<Track> audios; public IEnumerable<Track> Audios;
public IEnumerable<Track> subtitles; public IEnumerable<Track> Subtitles;
public WatchItem() { } public WatchItem() { }
public WatchItem(long episodeID, string showTitle, string showSlug, long seasonNumber, long episodeNumber, string title, DateTime? releaseDate, string path) 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; ShowTitle = showTitle;
ShowSlug = showSlug; ShowSlug = showSlug;
this.seasonNumber = seasonNumber; SeasonNumber = seasonNumber;
this.episodeNumber = episodeNumber; EpisodeNumber = episodeNumber;
Title = title; Title = title;
ReleaseDate = releaseDate; ReleaseDate = releaseDate;
Path = path; 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) 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; Audios = audios;
this.subtitles = subtitles; Subtitles = subtitles;
} }
public static WatchItem FromReader(System.Data.SQLite.SQLiteDataReader reader) public static WatchItem FromReader(System.Data.SQLite.SQLiteDataReader reader)
@ -61,35 +61,35 @@ namespace Kyoo.Models
public WatchItem SetStreams(ILibraryManager libraryManager) public WatchItem SetStreams(ILibraryManager libraryManager)
{ {
(Track video, IEnumerable<Track> audios, IEnumerable<Track> subtitles) streams = libraryManager.GetStreams(episodeID, Link); (Track video, IEnumerable<Track> audios, IEnumerable<Track> subtitles) streams = libraryManager.GetStreams(EpisodeID, Link);
container = Path.Substring(Path.LastIndexOf('.') + 1); Container = Path.Substring(Path.LastIndexOf('.') + 1);
video = streams.video; Video = streams.video;
audios = streams.audios; Audios = streams.audios;
subtitles = streams.subtitles; Subtitles = streams.subtitles;
return this; return this;
} }
public WatchItem SetPrevious(ILibraryManager libraryManager) public WatchItem SetPrevious(ILibraryManager libraryManager)
{ {
long lastEp = episodeNumber - 1; long lastEp = EpisodeNumber - 1;
if(lastEp > 0) if(lastEp > 0)
previousEpisode = ShowSlug + "-s" + seasonNumber + "e" + lastEp; PreviousEpisode = ShowSlug + "-s" + SeasonNumber + "e" + lastEp;
else if(seasonNumber > 1) else if(SeasonNumber > 1)
{ {
int seasonCount = libraryManager.GetSeasonCount(ShowSlug, seasonNumber - 1); int seasonCount = libraryManager.GetSeasonCount(ShowSlug, SeasonNumber - 1);
previousEpisode = ShowSlug + "-s" + (seasonNumber - 1) + "e" + seasonCount; PreviousEpisode = ShowSlug + "-s" + (SeasonNumber - 1) + "e" + seasonCount;
} }
return this; return this;
} }
public WatchItem SetNext(ILibraryManager libraryManager) public WatchItem SetNext(ILibraryManager libraryManager)
{ {
long seasonCount = libraryManager.GetSeasonCount(ShowSlug, seasonNumber); long seasonCount = libraryManager.GetSeasonCount(ShowSlug, SeasonNumber);
if (episodeNumber >= seasonCount) if (EpisodeNumber >= seasonCount)
nextEpisode = libraryManager.GetEpisode(ShowSlug, seasonNumber + 1, 1); NextEpisode = libraryManager.GetEpisode(ShowSlug, SeasonNumber + 1, 1);
else else
nextEpisode = libraryManager.GetEpisode(ShowSlug, seasonNumber, episodeNumber + 1); NextEpisode = libraryManager.GetEpisode(ShowSlug, SeasonNumber, EpisodeNumber + 1);
return this; return this;
} }

62
Kyoo.Common/Utility.cs Normal file
View File

@ -0,0 +1,62 @@
using System.Text.RegularExpressions;
using Kyoo.Models;
namespace Kyoo
{
public interface IMergable<T>
{
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;
}
}
}
}

View File

@ -1,6 +1,8 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kyoo", "Kyoo\Kyoo.csproj", "{0F8275B6-C7DD-42DF-A168-755C81B1C329}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Kyoo", "Kyoo\Kyoo.csproj", "{0F8275B6-C7DD-42DF-A168-755C81B1C329}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kyoo.Common", "Kyoo.Common\Kyoo.Common.csproj", "{BAB2CAE1-AC28-4509-AA3E-8DC75BD59220}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU 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}.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.ActiveCfg = Release|Any CPU
{0F8275B6-C7DD-42DF-A168-755C81B1C329}.Release|Any CPU.Build.0 = 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 EndGlobalSection
EndGlobal EndGlobal

View File

@ -4,4 +4,8 @@
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String> <s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForOtherTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String> <s:String x:Key="/Default/CodeStyle/CSharpVarKeywordUsage/ForSimpleTypes/@EntryValue">UseExplicitType</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=API/@EntryIndexedValue">API</s:String> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=API/@EntryIndexedValue">API</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=DB/@EntryIndexedValue">DB</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

269
Kyoo/Controllers/Crawler.cs Normal file
View File

@ -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<Episode> episodes = libraryManager.GetAllEpisodes();
IEnumerable<Library> 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<string>("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<string>("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<Show> 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<People> 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<long> 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;
}
}
}

View File

@ -7,7 +7,7 @@ using System.Data.SQLite;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
namespace Kyoo.InternalAPI namespace Kyoo.Controllers
{ {
public class LibraryManager : ILibraryManager public class LibraryManager : ILibraryManager
{ {
@ -18,10 +18,9 @@ namespace Kyoo.InternalAPI
{ {
string databasePath = configuration.GetValue<string>("databasePath"); string databasePath = configuration.GetValue<string>("databasePath");
Debug.WriteLine("&Library Manager init, databasePath: " + databasePath);
if (!File.Exists(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))) if (!Directory.Exists(Path.GetDirectoryName(databasePath)))
Directory.CreateDirectory(databasePath); Directory.CreateDirectory(databasePath);
@ -29,7 +28,7 @@ namespace Kyoo.InternalAPI
sqlConnection = new SQLiteConnection($"Data Source={databasePath};Version=3"); sqlConnection = new SQLiteConnection($"Data Source={databasePath};Version=3");
sqlConnection.Open(); sqlConnection.Open();
string createStatement = @"CREATE TABLE shows( const string createStatement = @"CREATE TABLE shows(
id INTEGER PRIMARY KEY UNIQUE, id INTEGER PRIMARY KEY UNIQUE,
slug TEXT UNIQUE, slug TEXT UNIQUE,
title TEXT, title TEXT,
@ -92,7 +91,8 @@ namespace Kyoo.InternalAPI
id INTEGER PRIMARY KEY UNIQUE, id INTEGER PRIMARY KEY UNIQUE,
slug TEXT UNIQUE, slug TEXT UNIQUE,
name TEXT, name TEXT,
path TEXT path TEXT,
providers TEXT
); );
CREATE TABLE librariesLinks( CREATE TABLE librariesLinks(
libraryID INTEGER, libraryID INTEGER,
@ -209,10 +209,11 @@ namespace Kyoo.InternalAPI
public string GetShowExternalIDs(long showID) 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)) using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{ {
cmd.Parameters.AddWithValue("$showID", showID);
SQLiteDataReader reader = cmd.ExecuteReader(); SQLiteDataReader reader = cmd.ExecuteReader();
if (reader.Read()) if (reader.Read())
@ -835,7 +836,7 @@ namespace Kyoo.InternalAPI
Genre existingGenre = GetGenreBySlug(genre.Slug); Genre existingGenre = GetGenreBySlug(genre.Slug);
if (existingGenre != null) if (existingGenre != null)
return existingGenre.id; return existingGenre.ID;
string query = "INSERT INTO genres (slug, name) VALUES($slug, $name);"; string query = "INSERT INTO genres (slug, name) VALUES($slug, $name);";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
@ -864,7 +865,7 @@ namespace Kyoo.InternalAPI
Studio existingStudio = GetStudioBySlug(studio.Slug); Studio existingStudio = GetStudioBySlug(studio.Slug);
if (existingStudio != null) if (existingStudio != null)
return existingStudio.id; return existingStudio.ID;
string query = "INSERT INTO studios (slug, name) VALUES($slug, $name);"; string query = "INSERT INTO studios (slug, name) VALUES($slug, $name);";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
@ -890,20 +891,20 @@ namespace Kyoo.InternalAPI
public long GetOrCreatePeople(People people) public long GetOrCreatePeople(People people)
{ {
People existingPeople = GetPeopleBySlug(people.slug); People existingPeople = GetPeopleBySlug(people.Slug);
if (existingPeople != null) if (existingPeople != null)
return existingPeople.id; return existingPeople.ID;
string query = "INSERT INTO people (slug, name, imgPrimary, externalIDs) VALUES($slug, $name, $imgPrimary, $externalIDs);"; string query = "INSERT INTO people (slug, name, imgPrimary, externalIDs) VALUES($slug, $name, $imgPrimary, $externalIDs);";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{ {
try try
{ {
cmd.Parameters.AddWithValue("$slug", people.slug); cmd.Parameters.AddWithValue("$slug", people.Slug);
cmd.Parameters.AddWithValue("$name", people.Name); cmd.Parameters.AddWithValue("$name", people.Name);
cmd.Parameters.AddWithValue("$imgPrimary", people.imgPrimary); cmd.Parameters.AddWithValue("$imgPrimary", people.ImgPrimary);
cmd.Parameters.AddWithValue("$externalIDs", people.externalIDs); cmd.Parameters.AddWithValue("$externalIDs", people.ExternalIDs);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT LAST_INSERT_ROWID()"; 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); Console.Error.WriteLine("SQL error while trying to insert a people ({0}).", people.Name);
cmd.CommandText = "SELECT * FROM people WHERE slug = $slug"; cmd.CommandText = "SELECT * FROM people WHERE slug = $slug";
cmd.Parameters.AddWithValue("$slug", people.slug); cmd.Parameters.AddWithValue("$slug", people.Slug);
return (long)cmd.ExecuteScalar(); 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)) using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{ {
cmd.Parameters.AddWithValue("$libraryPath", libraryPath); cmd.Parameters.AddWithValue("$libraryID", library.ID);
cmd.Parameters.AddWithValue("$showID", showID); cmd.Parameters.AddWithValue("$showID", showID);
cmd.ExecuteNonQuery(); 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);"; 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("$studioID", studioID);
cmd.Parameters.AddWithValue("$showID", showID); cmd.Parameters.AddWithValue("$showID", showID);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
@ -1026,10 +1028,10 @@ namespace Kyoo.InternalAPI
try try
{ {
cmd.Parameters.AddWithValue("$showID", season.ShowID); 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("$title", season.Title);
cmd.Parameters.AddWithValue("$overview", season.Overview); 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("$imgPrimary", season.ImgPrimary);
cmd.Parameters.AddWithValue("$externalIDs", season.ExternalIDs); cmd.Parameters.AddWithValue("$externalIDs", season.ExternalIDs);
cmd.ExecuteNonQuery(); 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); 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.CommandText = "SELECT * FROM seasons WHERE showID = $showID AND seasonNumber = $seasonNumber";
cmd.Parameters.AddWithValue("$showID", season.ShowID); cmd.Parameters.AddWithValue("$showID", season.ShowID);
cmd.Parameters.AddWithValue("$seasonNumber", season.seasonNumber); cmd.Parameters.AddWithValue("$seasonNumber", season.SeasonNumber);
return (long)cmd.ExecuteScalar(); return (long)cmd.ExecuteScalar();
} }
} }
@ -1057,9 +1059,9 @@ namespace Kyoo.InternalAPI
{ {
cmd.Parameters.AddWithValue("$showID", episode.ShowID); cmd.Parameters.AddWithValue("$showID", episode.ShowID);
cmd.Parameters.AddWithValue("$seasonID", episode.SeasonID); cmd.Parameters.AddWithValue("$seasonID", episode.SeasonID);
cmd.Parameters.AddWithValue("$seasonNUmber", episode.seasonNumber); cmd.Parameters.AddWithValue("$seasonNUmber", episode.SeasonNumber);
cmd.Parameters.AddWithValue("$episodeNumber", episode.episodeNumber); cmd.Parameters.AddWithValue("$episodeNumber", episode.EpisodeNumber);
cmd.Parameters.AddWithValue("$absoluteNumber", episode.absoluteNumber); cmd.Parameters.AddWithValue("$absoluteNumber", episode.AbsoluteNumber);
cmd.Parameters.AddWithValue("$path", episode.Path); cmd.Parameters.AddWithValue("$path", episode.Path);
cmd.Parameters.AddWithValue("$title", episode.Title); cmd.Parameters.AddWithValue("$title", episode.Title);
cmd.Parameters.AddWithValue("$overview", episode.Overview); 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); 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.CommandText = "SELECT * FROM episodes WHERE showID = $showID AND seasonNumber = $seasonNumber AND episodeNumber = $episodeNumber";
cmd.Parameters.AddWithValue("$showID", episode.ShowID); cmd.Parameters.AddWithValue("$showID", episode.ShowID);
cmd.Parameters.AddWithValue("$seasonNumber", episode.seasonNumber); cmd.Parameters.AddWithValue("$seasonNumber", episode.SeasonNumber);
cmd.Parameters.AddWithValue("$episodeNumber", episode.episodeNumber); cmd.Parameters.AddWithValue("$episodeNumber", episode.EpisodeNumber);
return (long)cmd.ExecuteScalar(); 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);"; 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)) 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("$streamType", track.Type);
cmd.Parameters.AddWithValue("$title", track.Title); cmd.Parameters.AddWithValue("$title", track.Title);
cmd.Parameters.AddWithValue("$language", track.Language); cmd.Parameters.AddWithValue("$language", track.Language);
@ -1102,21 +1104,21 @@ namespace Kyoo.InternalAPI
} }
} }
public void RegisterShowPeople(long showID, List<People> people) public void RegisterShowPeople(long showID, IEnumerable<People> people)
{ {
if (people == null) if (people == null)
return; return;
string linkQuery = "INSERT INTO peopleLinks (peopleID, showID, role, type) VALUES($peopleID, $showID, $role, $type);"; 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)) 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("$showID", showID);
cmd.Parameters.AddWithValue("$role", people[i].Role); cmd.Parameters.AddWithValue("$role", peop.Role);
cmd.Parameters.AddWithValue("$type", people[i].Type); cmd.Parameters.AddWithValue("$type", peop.Type);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
} }
@ -1164,11 +1166,11 @@ namespace Kyoo.InternalAPI
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection)) using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{ {
cmd.Parameters.AddWithValue("$episodeID", episode.id); cmd.Parameters.AddWithValue("$episodeID", episode.ID);
cmd.ExecuteNonQuery(); cmd.ExecuteNonQuery();
} }
if (GetEpisodes(episode.ShowID, episode.seasonNumber).Count == 0) if (GetEpisodes(episode.ShowID, episode.SeasonNumber).Count == 0)
RemoveSeason(episode.ShowID, episode.SeasonID); RemoveSeason(episode.ShowID, episode.SeasonID);
} }

View File

@ -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<IPlugin> plugins;
public PluginManager(IServiceProvider provider, IConfiguration config)
{
this.provider = provider;
this.config = config;
}
public T GetPlugin<T>(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<T> GetPlugins<T>()
{
if (plugins == null)
return null;
return from plugin in plugins where plugin is T
select (T)plugin;
}
public void ReloadPlugins()
{
string pluginFolder = config.GetValue<string>("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();
}
}
}

View File

@ -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<IMetadataProvider> providers;
private readonly IThumbnailsManager thumbnailsManager;
public ProviderManager(IThumbnailsManager thumbnailsManager, IPluginManager pluginManager)
{
this.thumbnailsManager = thumbnailsManager;
providers = pluginManager.GetPlugins<IMetadataProvider>();
}
public async Task<T> GetMetadata<T>(Func<IMetadataProvider, Task<T>> providerCall, Library library, string what) where T : IMergable<T>, 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<IEnumerable<T>> GetMetadata<T>(Func<IMetadataProvider, Task<IEnumerable<T>>> providerCall, Library library, string what)
{
List<T> ret = new List<T>();
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<Collection> GetCollectionFromName(string name, Library library)
{
return await GetMetadata(provider => provider.GetCollectionFromName(name), library, $"the collection {name}");
}
public async Task<Show> 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<Season> 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<Episode> 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<IEnumerable<People>> GetPeople(Show show, Library library)
{
IEnumerable<People> people = await GetMetadata(provider => provider.GetPeople(show), library, "unknown data");
people = await thumbnailsManager.Validate(people);
return people;
}
}
}

View File

@ -7,7 +7,7 @@ using System.IO;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace Kyoo.InternalAPI.ThumbnailsManager namespace Kyoo.Controllers.ThumbnailsManager
{ {
public class ThumbnailsManager : IThumbnailsManager public class ThumbnailsManager : IThumbnailsManager
{ {
@ -20,6 +20,8 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
public async Task<Show> Validate(Show show) public async Task<Show> Validate(Show show)
{ {
if (show == null || show.Path == null)
return null;
string localThumb = Path.Combine(show.Path, "poster.jpg"); string localThumb = Path.Combine(show.Path, "poster.jpg");
string localLogo = Path.Combine(show.Path, "logo.png"); string localLogo = Path.Combine(show.Path, "logo.png");
string localBackdrop = Path.Combine(show.Path, "backdrop.jpg"); string localBackdrop = Path.Combine(show.Path, "backdrop.jpg");
@ -67,25 +69,26 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
return show; return show;
} }
public async Task<List<People>> Validate(List<People> people) public async Task<IEnumerable<People>> Validate(IEnumerable<People> people)
{ {
for (int i = 0; i < people?.Count; i++) if (people == null)
return null;
foreach (People peop in people)
{ {
string root = config.GetValue<string>("peoplePath"); string root = config.GetValue<string>("peoplePath");
Directory.CreateDirectory(root); Directory.CreateDirectory(root);
string localThumb = root + "/" + people[i].slug + ".jpg"; string localThumb = root + "/" + peop.Slug + ".jpg";
if (people[i].imgPrimary != null && !File.Exists(localThumb)) if (peop.ImgPrimary == null || File.Exists(localThumb))
continue;
try
{ {
try using WebClient client = new WebClient();
{ await client.DownloadFileTaskAsync(new Uri(peop.ImgPrimary), localThumb);
using WebClient client = new WebClient(); }
await client.DownloadFileTaskAsync(new Uri(people[i].imgPrimary), localThumb); catch (WebException)
} {
catch (WebException) Console.Error.WriteLine("Couldn't download an image.");
{
Console.Error.WriteLine("Couldn't download an image.");
}
} }
} }
@ -94,18 +97,19 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
public async Task<Episode> Validate(Episode episode) public async Task<Episode> Validate(Episode episode)
{ {
string localThumb = Path.ChangeExtension(episode.Path, "jpg"); if (episode == null || episode.Path == null)
if (episode.ImgPrimary != null && !File.Exists(localThumb)) 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);
using WebClient client = new WebClient(); }
await client.DownloadFileTaskAsync(new Uri(episode.ImgPrimary), localThumb); catch (WebException)
} {
catch (WebException) Console.Error.WriteLine("Couldn't download an image.");
{
Console.Error.WriteLine("Couldn't download an image.");
}
} }
return episode; return episode;

View File

@ -1,14 +1,15 @@
using System; using System;
using Kyoo.Models; using Kyoo.Models;
using Kyoo.InternalAPI.TranscoderLink;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Kyoo.Controllers.TranscoderLink;
#pragma warning disable 4014 #pragma warning disable 4014
namespace Kyoo.InternalAPI namespace Kyoo.Controllers
{ {
public class Transcoder : ITranscoder public class Transcoder : ITranscoder
{ {

View File

@ -5,7 +5,7 @@ using Kyoo.Models;
using Kyoo.Models.Watch; using Kyoo.Models.Watch;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace Kyoo.InternalAPI.TranscoderLink namespace Kyoo.Controllers.TranscoderLink
{ {
public static class TranscoderAPI public static class TranscoderAPI
{ {

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -18,10 +18,10 @@ namespace Kyoo.Controllers
this.crawler = crawler; this.crawler = crawler;
} }
[HttpGet("scan/{watch}")] [HttpGet("scan")]
public IActionResult ScanLibrary(bool watch) public IActionResult ScanLibrary()
{ {
crawler.Start(watch); crawler.Start();
return Ok("Scanning"); return Ok("Scanning");
} }
} }

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;
@ -30,7 +30,7 @@ namespace Kyoo.Controllers
if (library == null) if (library == null)
return NotFound(); return NotFound();
return libraryManager.GetShowsInLibrary(library.id); return libraryManager.GetShowsInLibrary(library.ID);
} }
} }
} }

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Diagnostics; using System.Diagnostics;
@ -23,10 +23,10 @@ namespace Kyoo.Controllers
if (people == null) if (people == null)
return NotFound(); 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), Shows = libraryManager.GetShowsByPeople(people.ID),
Poster = "peopleimg/" + people.slug Poster = "peopleimg/" + people.Slug
}; };
return collection; return collection;
} }

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -20,12 +20,12 @@ namespace Kyoo.Controllers
{ {
SearchResult result = new SearchResult SearchResult result = new SearchResult
{ {
query = query, Query = query,
shows = libraryManager.GetShows(query), Shows = libraryManager.GetShows(query),
episodes = libraryManager.SearchEpisodes(query), Episodes = libraryManager.SearchEpisodes(query),
people = libraryManager.SearchPeople(query), People = libraryManager.SearchPeople(query),
genres = libraryManager.SearchGenres(query), Genres = libraryManager.SearchGenres(query),
studios = libraryManager.SearchStudios(query) Studios = libraryManager.SearchStudios(query)
}; };
return result; return result;
} }

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;

View File

@ -1,8 +1,6 @@
using Kyoo.InternalAPI; using Kyoo.Models;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -52,12 +50,12 @@ namespace Kyoo.Controllers
public async Task<string> ExtractSubtitle(string showSlug, long seasonNumber, long episodeNumber) public async Task<string> ExtractSubtitle(string showSlug, long seasonNumber, long episodeNumber)
{ {
Episode episode = libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber); Episode episode = libraryManager.GetEpisode(showSlug, seasonNumber, episodeNumber);
libraryManager.ClearSubtitles(episode.id); libraryManager.ClearSubtitles(episode.ID);
Track[] tracks = await transcoder.ExtractSubtitles(episode.Path); Track[] tracks = await transcoder.ExtractSubtitles(episode.Path);
foreach (Track track in tracks) foreach (Track track in tracks)
{ {
track.episodeID = episode.id; track.EpisodeID = episode.ID;
libraryManager.RegisterTrack(track); libraryManager.RegisterTrack(track);
} }
@ -70,12 +68,12 @@ namespace Kyoo.Controllers
List<Episode> episodes = libraryManager.GetEpisodes(showSlug); List<Episode> episodes = libraryManager.GetEpisodes(showSlug);
foreach (Episode episode in episodes) foreach (Episode episode in episodes)
{ {
libraryManager.ClearSubtitles(episode.id); libraryManager.ClearSubtitles(episode.ID);
Track[] tracks = await transcoder.ExtractSubtitles(episode.Path); Track[] tracks = await transcoder.ExtractSubtitles(episode.Path);
foreach (Track track in tracks) foreach (Track track in tracks)
{ {
track.episodeID = episode.id; track.EpisodeID = episode.ID;
libraryManager.RegisterTrack(track); libraryManager.RegisterTrack(track);
} }
} }

View File

@ -1,11 +1,10 @@
using Kyoo.InternalAPI; using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using System.IO; using System.IO;
namespace Kyoo.Controllers namespace Kyoo.Controllers
{ {
public class ThumbnailController : Controller public class ThumbnailController : ControllerBase
{ {
private readonly ILibraryManager libraryManager; private readonly ILibraryManager libraryManager;
private readonly string peoplePath; private readonly string peoplePath;

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.Models; using Kyoo.Models;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Diagnostics; using System.Diagnostics;

View File

@ -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<Episode> episodes = libraryManager.GetAllEpisodes();
IEnumerable<string> 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<string>("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<string>("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<Show> 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<People> 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;
}
}
}

View File

@ -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<Collection> GetCollectionFromName(string name);
//For the show
Task<Show> GetShowByID(string id);
Task<Show> GetShowFromName(string showName, string showPath);
Task<Show> GetImages(Show show);
Task<List<People>> GetPeople(string id);
//For the seasons
Task<Season> GetSeason(string showName, long seasonNumber);
Task<string> GetSeasonImage(string showName, long seasonNumber);
//For the episodes
Task<Episode> GetEpisode(string externalIDs, long seasonNumber, long episodeNumber, long absoluteNumber, string episodePath);
}
}

View File

@ -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<string> 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;
}
}
}

View File

@ -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<Collection> GetCollectionFromName(string name)
{
return new Collection(-1, Slugifier.ToSlug(name), name, null, null);
}
public async Task<Show> 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<IEnumerable<string>>(),
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<Show> 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<IEnumerable<string>>(),
null, //Path
(string) data.overview,
null, //Trailer
GetGenres(((JArray) data.genre).ToObject<string[]>()),
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<Show> 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<ImageType, string> imageTypes = new Dictionary<ImageType, string> { { ImageType.Poster, "poster" }, { ImageType.Background, "fanart" } };
foreach (KeyValuePair<ImageType, string> 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<dynamic>) 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<Season> GetSeason(string showName, long seasonNumber)
{
return new Season(-1, -1, seasonNumber, "Season " + seasonNumber, null, null, null, null);
}
public Task<string> GetSeasonImage(string showName, long seasonNumber)
{
return null;
}
public async Task<Episode> 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<List<People>> 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<dynamic>)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;
}
}
}
}

View File

@ -1,13 +0,0 @@
using System;
namespace Kyoo.InternalAPI.MetadataProvider
{
[AttributeUsage(AttributeTargets.Class)]
public class MetaProvider : Attribute
{
public MetaProvider()
{
}
}
}

View File

@ -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<Genre> GetGenres(string[] input)
{
List<Genre> genres = new List<Genre>();
foreach (string genre in input)
genres.Add(new Genre(ToSlug(genre), genre));
return genres;
}
}
}

View File

@ -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<IMetadataProvider> providers = new List<IMetadataProvider>();
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<string>("providerPlugins");
if (Directory.Exists(pluginFolder))
{
string[] pluginsPaths = Directory.GetFiles(pluginFolder);
List<Assembly> plugins = new List<Assembly>();
List<Type> types = new List<Type>();
for (int i = 0; i < pluginsPaths.Length; i++)
{
plugins.Add(Assembly.LoadFile(pluginsPaths[i]));
types.AddRange(plugins[i].GetTypes());
}
List<Type> providersPlugins = types.FindAll(x =>
{
object[] atr = x.GetCustomAttributes(typeof(MetaProvider), false);
if (atr == null || atr.Length == 0)
return false;
List<Type> interfaces = new List<Type>(x.GetInterfaces());
if (interfaces.Contains(typeof(IMetadataProvider)))
return true;
return false;
});
providers.AddRange(providersPlugins.ConvertAll<IMetadataProvider>(x => Activator.CreateInstance(x) as IMetadataProvider));
}
}
public Show Merge(IEnumerable<Show> shows)
{
return shows.FirstOrDefault();
}
public Season Merge(IEnumerable<Season> seasons)
{
return seasons.FirstOrDefault();
}
public Episode Merge(IEnumerable<Episode> 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<Collection> GetCollectionFromName(string name)
{
return providers[0].GetCollectionFromName(name);
}
public Task<Show> GetImages(Show show)
{
return providers[0].GetImages(show);
}
public async Task<Season> GetSeason(string showName, int seasonNumber)
{
List<Season> datas = new List<Season>();
for (int i = 0; i < providers.Count; i++)
{
datas.Add(await providers[i].GetSeason(showName, seasonNumber));
}
return Merge(datas);
}
public async Task<Show> GetShowByID(string id)
{
List<Show> datas = new List<Show>();
for (int i = 0; i < providers.Count; i++)
{
datas.Add(await providers[i].GetShowByID(id));
}
return Merge(datas);
}
public async Task<Show> GetShowFromName(string showName, string showPath)
{
List<Show> datas = new List<Show>();
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<Season> GetSeason(string showName, long seasonNumber)
{
List<Season> datas = new List<Season>();
for (int i = 0; i < providers.Count; i++)
{
datas.Add(await providers[i].GetSeason(showName, seasonNumber));
}
return Merge(datas);
}
public Task<string> GetSeasonImage(string showName, long seasonNumber)
{
//Should select the best provider for this show.
return providers[0].GetSeasonImage(showName, seasonNumber);
}
public async Task<Episode> GetEpisode(string externalIDs, long seasonNumber, long episodeNumber, long absoluteNumber, string episodePath)
{
List<Episode> datas = new List<Episode>();
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<List<People>> GetPeople(string id)
{
List<People> actors = await providers[0].GetPeople(id);
return await thumbnailsManager.Validate(actors);
}
}
}

View File

@ -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;
}
}
}

View File

@ -17,6 +17,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" /> <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.0.0" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.0.0" /> <PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.0.0" />
@ -29,6 +30,8 @@
<Content Remove="$(SpaRoot)**" /> <Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" /> <None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" /> <None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
<None Remove="Controllers\MetadataProvider\**" />
<Content Remove="Controllers\MetadataProvider\**" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -44,6 +47,15 @@
<EmbeddedResource Include="kyoo.sh"> <EmbeddedResource Include="kyoo.sh">
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource> </EmbeddedResource>
<EmbeddedResource Remove="Controllers\MetadataProvider\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Kyoo.Common\Kyoo.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Controllers\MetadataProvider\**" />
</ItemGroup> </ItemGroup>
<Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') "> <Target Name="DebugEnsureNodeEnv" BeforeTargets="Build" Condition=" '$(Configuration)' == 'Debug' And !Exists('$(SpaRoot)node_modules') ">

View File

@ -1,4 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controllers_005Cmetadataprovider_005Cimplementations/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controllers_005Cmetadataprovider_005Cimplementations_005Cthetvdb/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=controllers_005Ctranscoder/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=internalapi_005Ccrawler/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=internalapi_005Ccrawler/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=internalapi_005Clibrarymanager/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=internalapi_005Clibrarymanager/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=internalapi_005Cmetadataprovider_005Cimplementations/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=internalapi_005Cmetadataprovider_005Cimplementations/@EntryIndexedValue">True</s:Boolean>

View File

@ -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<Show> 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;
}
}
}

View File

@ -1,14 +0,0 @@
using System.Collections.Generic;
namespace Kyoo.Models
{
public class SearchResult
{
public string query;
public IEnumerable<Show> shows;
public IEnumerable<Episode> episodes;
public IEnumerable<People> people;
public IEnumerable<Genre> genres;
public IEnumerable<Studio> studios;
}
}

View File

@ -1,14 +1,27 @@
using System.Threading.Tasks;
using Kyoo.Controllers;
using Microsoft.AspNetCore; using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Hosting;
namespace Kyoo namespace Kyoo
{ {
public class Program 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<IPluginManager>();
pluginManager.ReloadPlugins();
ICrawler crawler = serviceScope.ServiceProvider.GetService<ICrawler>();
crawler.Start();
}
await host.RunAsync();
} }
public static IWebHostBuilder CreateWebHostBuilder(string[] args) => public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>

View File

@ -1,5 +1,5 @@
using Kyoo.InternalAPI; using Kyoo.Controllers;
using Kyoo.InternalAPI.ThumbnailsManager; using Kyoo.Controllers.ThumbnailsManager;
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SpaServices.AngularCli; using Microsoft.AspNetCore.SpaServices.AngularCli;
@ -30,15 +30,14 @@ namespace Kyoo
}); });
services.AddControllers().AddNewtonsoftJson(); services.AddControllers().AddNewtonsoftJson();
services.AddHttpClient();
//Services needed in the private and in the public API
services.AddSingleton<ILibraryManager, LibraryManager>(); services.AddSingleton<ILibraryManager, LibraryManager>();
services.AddSingleton<ITranscoder, Transcoder>(); services.AddSingleton<ITranscoder, Transcoder>();
//Services used to get informations about files and register them
services.AddSingleton<ICrawler, Crawler>();
services.AddSingleton<IThumbnailsManager, ThumbnailsManager>(); services.AddSingleton<IThumbnailsManager, ThumbnailsManager>();
services.AddSingleton<IMetadataProvider, ProviderManager>(); services.AddSingleton<IProviderManager, ProviderManager>();
services.AddSingleton<ICrawler, Crawler>();
services.AddSingleton<IPluginManager, PluginManager>();
} }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

View File

@ -12,7 +12,7 @@
"transmuxTempPath": "/tmp/cached/kyoo/transmux", "transmuxTempPath": "/tmp/cached/kyoo/transmux",
"transcodeTempPath": "/tmp/cached/kyoo/transcode", "transcodeTempPath": "/tmp/cached/kyoo/transcode",
"peoplePath": "/tmp/people", "peoplePath": "/tmp/people",
"plugins": "/tmp/plugins", "plugins": "plugins/",
"regex": "^(\\/(?<Collection>.+?))?\\/.*\\/(?<ShowTitle>.+?) S(?<Season>\\d+)E(?<Episode>\\d+)", "regex": "^(\\/(?<Collection>.+?))?\\/.*\\/(?<ShowTitle>.+?) S(?<Season>\\d+)E(?<Episode>\\d+)",
"absoluteRegex": ".*\\/(?<ShowTitle>.+?) (?<AbsoluteNumber>\\d+)" "absoluteRegex": ".*\\/(?<ShowTitle>.+?) (?<AbsoluteNumber>\\d+)"
} }

View File

@ -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);
}
}
}

View File

@ -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 + ")");
}
}
}

View File

@ -1,29 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp3.1</TargetFramework>
<RootNamespace>UnitTests</RootNamespace>
<IsPackable>false</IsPackable>
<AssemblyName>UnitTests</AssemblyName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="3.0.0" />
<PackageReference Include="nunit" Version="3.12.0" />
<PackageReference Include="NUnit3TestAdapter" Version="3.13.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Kyoo\Kyoo.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

View File

@ -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": "^(\\\\(?<Collection>.+?))?\\\\.*\\\\(?<ShowTitle>.+?) S(?<Season>\\d+)E(?<Episode>\\d+)",
"absoluteRegex": ".*\\\\(?<ShowTitle>.+?) (?<AbsoluteNumber>\\d+)"
}

@ -1 +1 @@
Subproject commit d8f72ff87020f2d38a4552f6083ef0659bf1f2be Subproject commit bedf98f85d2cf669a1d1c4af35271841133cfec2