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.Tasks;
namespace Kyoo.InternalAPI
namespace Kyoo.Controllers
{
public interface ICrawler
{
Task Start(bool watch);
void Start();
Task StopAsync();
void Cancel();
}
}

View File

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

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.Threading.Tasks;
namespace Kyoo.InternalAPI.ThumbnailsManager
namespace Kyoo.Controllers.ThumbnailsManager
{
public interface IThumbnailsManager
{
Task<Show> Validate(Show show);
Task<List<People>> Validate(List<People> actors);
Task<IEnumerable<People>> Validate(IEnumerable<People> actors);
Task<Episode> Validate(Episode episode);
}
}

View File

@ -2,7 +2,7 @@ using Kyoo.Models;
using Kyoo.Models.Watch;
using System.Threading.Tasks;
namespace Kyoo.InternalAPI
namespace Kyoo.Controllers
{
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
{
public class Episode
public class Episode : IMergable<Episode>
{
[JsonIgnore] public long id;
[JsonIgnore] public long ID;
[JsonIgnore] public long ShowID;
[JsonIgnore] public long SeasonID;
public long seasonNumber;
public long episodeNumber;
public long absoluteNumber;
public long SeasonNumber;
public long EpisodeNumber;
public long AbsoluteNumber;
[JsonIgnore] public string Path;
public string Title;
public string Overview;
@ -27,16 +27,24 @@ namespace Kyoo.Models
public string Thumb; //Used in the API response only
public Episode() { }
public Episode()
{
ID = -1;
ShowID = -1;
SeasonID = -1;
SeasonNumber = -1;
EpisodeNumber = -1;
AbsoluteNumber = -1;
}
public Episode(long seasonNumber, long episodeNumber, long absoluteNumber, string title, string overview, DateTime? releaseDate, long runtime, string imgPrimary, string externalIDs)
{
id = -1;
ID = -1;
ShowID = -1;
SeasonID = -1;
this.seasonNumber = seasonNumber;
this.episodeNumber = episodeNumber;
this.absoluteNumber = absoluteNumber;
SeasonNumber = seasonNumber;
EpisodeNumber = episodeNumber;
AbsoluteNumber = absoluteNumber;
Title = title;
Overview = overview;
ReleaseDate = releaseDate;
@ -47,12 +55,12 @@ namespace Kyoo.Models
public Episode(long id, long showID, long seasonID, long seasonNumber, long episodeNumber, long absoluteNumber, string path, string title, string overview, DateTime? releaseDate, long runtime, string imgPrimary, string externalIDs)
{
this.id = id;
ID = id;
ShowID = showID;
SeasonID = seasonID;
this.seasonNumber = seasonNumber;
this.episodeNumber = episodeNumber;
this.absoluteNumber = absoluteNumber;
SeasonNumber = seasonNumber;
EpisodeNumber = episodeNumber;
AbsoluteNumber = absoluteNumber;
Path = path;
Title = title;
Overview = overview;
@ -82,7 +90,7 @@ namespace Kyoo.Models
public Episode SetThumb(string showSlug)
{
Link = GetSlug(showSlug, seasonNumber, episodeNumber);
Link = GetSlug(showSlug, SeasonNumber, EpisodeNumber);
Thumb = "thumb/" + Link;
return this;
}
@ -97,5 +105,37 @@ namespace Kyoo.Models
{
return showSlug + "-s" + seasonNumber + "e" + episodeNumber;
}
public Episode Merge(Episode other)
{
if (other == null)
return this;
if (ID == -1)
ID = other.ID;
if (ShowID == -1)
ShowID = other.ShowID;
if (SeasonID == -1)
SeasonID = other.SeasonID;
if (SeasonNumber == -1)
SeasonNumber = other.SeasonNumber;
if (EpisodeNumber == -1)
EpisodeNumber = other.EpisodeNumber;
if (AbsoluteNumber == -1)
AbsoluteNumber = other.AbsoluteNumber;
if (Path == null)
Path = other.Path;
if (Title == null)
Title = other.Title;
if (Overview == null)
Overview = other.Overview;
if (ReleaseDate == null)
ReleaseDate = other.ReleaseDate;
if (Runtime == -1)
Runtime = other.Runtime;
if (ImgPrimary == null)
ImgPrimary = other.ImgPrimary;
ExternalIDs += '|' + other.ExternalIDs;
return this;
}
}
}

View File

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

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

View File

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

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

View File

@ -1,12 +1,14 @@
using Kyoo.InternalAPI;
using System;
using Kyoo.Controllers;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace Kyoo.Models
{
public class Show
public class Show : IMergable<Show>
{
[JsonIgnore] public long id = -1;
[JsonIgnore] public long ID = -1;
public string Slug;
public string Title;
@ -28,27 +30,21 @@ namespace Kyoo.Models
public string ExternalIDs;
//Used in the rest API excusively.
public Studio studio;
public IEnumerable<People> directors;
public IEnumerable<People> people;
public IEnumerable<Season> seasons;
public Studio Studio;
public IEnumerable<People> Directors;
public IEnumerable<People> People;
public IEnumerable<Season> Seasons;
public bool IsCollection;
public string GetAliases()
{
if (Aliases == null)
return null;
return string.Join('|', Aliases);
return Aliases == null ? null : string.Join('|', Aliases);
}
public string GetGenres()
{
if (Genres == null)
return null;
return string.Join('|', Genres);
return Genres == null ? null : string.Join('|', Genres);
}
@ -56,7 +52,7 @@ namespace Kyoo.Models
public Show(long id, string slug, string title, IEnumerable<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;
Title = title;
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)
{
this.id = id;
ID = id;
Slug = slug;
Title = title;
Aliases = aliases;
@ -111,7 +107,7 @@ namespace Kyoo.Models
return new Show((long)reader["id"],
reader["slug"] as string,
reader["title"] as string,
(reader["aliases"] as string)?.Split('|') ?? null,
(reader["aliases"] as string)?.Split('|'),
reader["path"] as string,
reader["overview"] as string,
reader["trailerUrl"] as string,
@ -125,6 +121,16 @@ namespace Kyoo.Models
reader["externalIDs"] as string);
}
public string GetID(string provider)
{
if (ExternalIDs?.Contains(provider) != true)
return null;
int startIndex = ExternalIDs.IndexOf(provider, StringComparison.Ordinal) + provider.Length + 1; //The + 1 is for the '='
if (ExternalIDs.IndexOf('|', startIndex) == -1)
return ExternalIDs.Substring(startIndex);
return ExternalIDs.Substring(startIndex, ExternalIDs.IndexOf('|', startIndex) - startIndex);
}
public Show Set(string slug, string path)
{
Slug = slug;
@ -134,31 +140,73 @@ namespace Kyoo.Models
public Show SetGenres(ILibraryManager manager)
{
Genres = manager.GetGenreForShow(id);
Genres = manager.GetGenreForShow(ID);
return this;
}
public Show SetStudio(ILibraryManager manager)
{
studio = manager.GetStudio(id);
Studio = manager.GetStudio(ID);
return this;
}
public Show SetDirectors(ILibraryManager manager)
{
directors = manager.GetDirectors(id);
Directors = manager.GetDirectors(ID);
return this;
}
public Show SetPeople(ILibraryManager manager)
{
people = manager.GetPeople(id);
People = manager.GetPeople(ID);
return this;
}
public Show SetSeasons(ILibraryManager manager)
{
seasons = manager.GetSeasons(id);
Seasons = manager.GetSeasons(ID);
return this;
}
public Show Merge(Show other)
{
if (other == null)
return this;
if (ID == -1)
ID = other.ID;
if (Slug == null)
Slug = other.Slug;
if (Title == null)
Title = other.Title;
if (Aliases == null)
Aliases = other.Aliases;
else
Aliases = Aliases.Concat(other.Aliases);
if (Genres == null)
Genres = other.Genres;
else
Genres = Genres.Concat(other.Genres);
if (Path == null)
Path = other.Path;
if (Overview == null)
Overview = other.Overview;
if (TrailerUrl == null)
TrailerUrl = other.TrailerUrl;
if (Status == null)
Status = other.Status;
if (StartYear == null)
StartYear = other.StartYear;
if (EndYear == null)
EndYear = other.EndYear;
if (ImgPrimary == null)
ImgPrimary = other.ImgPrimary;
if (ImgThumb == null)
ImgThumb = other.ImgThumb;
if (ImgLogo == null)
ImgLogo = other.ImgLogo;
if (ImgBackdrop == null)
ImgBackdrop = other.ImgBackdrop;
ExternalIDs += '|' + other.ExternalIDs;
return this;
}
}

View File

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

View File

@ -58,7 +58,7 @@ namespace Kyoo.Models
public string DisplayName;
public string Link;
[JsonIgnore] public long episodeID;
[JsonIgnore] public long EpisodeID;
[JsonIgnore] public bool IsExternal;
public Track(StreamType type, string title, string language, bool isDefault, bool isForced, string codec, bool isExternal, string path)
@ -94,7 +94,7 @@ namespace Kyoo.Models
if (language == "fre")
language = "fra";
DisplayName = CultureInfo.GetCultures(CultureTypes.NeutralCultures).FirstOrDefault(x => x.ThreeLetterISOLanguageName == language)?.DisplayName ?? language;
DisplayName = CultureInfo.GetCultures(CultureTypes.NeutralCultures).FirstOrDefault(x => x.ThreeLetterISOLanguageName == language)?.EnglishName ?? language;
Link = "/subtitle/" + episodeSlug + "." + Language;
if (IsForced)

View File

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

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

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/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/=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.IO;
namespace Kyoo.InternalAPI
namespace Kyoo.Controllers
{
public class LibraryManager : ILibraryManager
{
@ -18,10 +18,9 @@ namespace Kyoo.InternalAPI
{
string databasePath = configuration.GetValue<string>("databasePath");
Debug.WriteLine("&Library Manager init, databasePath: " + databasePath);
if (!File.Exists(databasePath))
{
Debug.WriteLine("&Database doesn't exist, creating one.");
Console.WriteLine($"Creating the database at {databasePath}.");
if (!Directory.Exists(Path.GetDirectoryName(databasePath)))
Directory.CreateDirectory(databasePath);
@ -29,7 +28,7 @@ namespace Kyoo.InternalAPI
sqlConnection = new SQLiteConnection($"Data Source={databasePath};Version=3");
sqlConnection.Open();
string createStatement = @"CREATE TABLE shows(
const string createStatement = @"CREATE TABLE shows(
id INTEGER PRIMARY KEY UNIQUE,
slug TEXT UNIQUE,
title TEXT,
@ -92,7 +91,8 @@ namespace Kyoo.InternalAPI
id INTEGER PRIMARY KEY UNIQUE,
slug TEXT UNIQUE,
name TEXT,
path TEXT
path TEXT,
providers TEXT
);
CREATE TABLE librariesLinks(
libraryID INTEGER,
@ -209,10 +209,11 @@ namespace Kyoo.InternalAPI
public string GetShowExternalIDs(long showID)
{
string query = string.Format("SELECT * FROM shows WHERE id = {0};", showID);
string query = "SELECT * FROM shows WHERE id = $showID;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$showID", showID);
SQLiteDataReader reader = cmd.ExecuteReader();
if (reader.Read())
@ -835,7 +836,7 @@ namespace Kyoo.InternalAPI
Genre existingGenre = GetGenreBySlug(genre.Slug);
if (existingGenre != null)
return existingGenre.id;
return existingGenre.ID;
string query = "INSERT INTO genres (slug, name) VALUES($slug, $name);";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
@ -864,7 +865,7 @@ namespace Kyoo.InternalAPI
Studio existingStudio = GetStudioBySlug(studio.Slug);
if (existingStudio != null)
return existingStudio.id;
return existingStudio.ID;
string query = "INSERT INTO studios (slug, name) VALUES($slug, $name);";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
@ -890,20 +891,20 @@ namespace Kyoo.InternalAPI
public long GetOrCreatePeople(People people)
{
People existingPeople = GetPeopleBySlug(people.slug);
People existingPeople = GetPeopleBySlug(people.Slug);
if (existingPeople != null)
return existingPeople.id;
return existingPeople.ID;
string query = "INSERT INTO people (slug, name, imgPrimary, externalIDs) VALUES($slug, $name, $imgPrimary, $externalIDs);";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
try
{
cmd.Parameters.AddWithValue("$slug", people.slug);
cmd.Parameters.AddWithValue("$slug", people.Slug);
cmd.Parameters.AddWithValue("$name", people.Name);
cmd.Parameters.AddWithValue("$imgPrimary", people.imgPrimary);
cmd.Parameters.AddWithValue("$externalIDs", people.externalIDs);
cmd.Parameters.AddWithValue("$imgPrimary", people.ImgPrimary);
cmd.Parameters.AddWithValue("$externalIDs", people.ExternalIDs);
cmd.ExecuteNonQuery();
cmd.CommandText = "SELECT LAST_INSERT_ROWID()";
@ -913,7 +914,7 @@ namespace Kyoo.InternalAPI
{
Console.Error.WriteLine("SQL error while trying to insert a people ({0}).", people.Name);
cmd.CommandText = "SELECT * FROM people WHERE slug = $slug";
cmd.Parameters.AddWithValue("$slug", people.slug);
cmd.Parameters.AddWithValue("$slug", people.Slug);
return (long)cmd.ExecuteScalar();
}
@ -949,13 +950,14 @@ namespace Kyoo.InternalAPI
}
}
public void RegisterInLibrary(long showID, string libraryPath)
public void RegisterInLibrary(long showID, Library library)
{
string query = "INSERT INTO librariesLinks (libraryID, showID) SELECT id, $showID FROM libraries WHERE libraries.path = $libraryPath;";
string query =
"INSERT INTO librariesLinks (libraryID, showID) SELECT id, $showID FROM libraries WHERE libraries.id = $libraryID;";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$libraryPath", libraryPath);
cmd.Parameters.AddWithValue("$libraryID", library.ID);
cmd.Parameters.AddWithValue("$showID", showID);
cmd.ExecuteNonQuery();
}
@ -999,10 +1001,10 @@ namespace Kyoo.InternalAPI
}
}
if (show.studio != null)
if (show.Studio != null)
{
cmd.CommandText = "INSERT INTO studiosLinks (studioID, showID) VALUES($studioID, $showID);";
long studioID = GetOrCreateStudio(show.studio);
long studioID = GetOrCreateStudio(show.Studio);
cmd.Parameters.AddWithValue("$studioID", studioID);
cmd.Parameters.AddWithValue("$showID", showID);
cmd.ExecuteNonQuery();
@ -1026,10 +1028,10 @@ namespace Kyoo.InternalAPI
try
{
cmd.Parameters.AddWithValue("$showID", season.ShowID);
cmd.Parameters.AddWithValue("$seasonNumber", season.seasonNumber);
cmd.Parameters.AddWithValue("$seasonNumber", season.SeasonNumber);
cmd.Parameters.AddWithValue("$title", season.Title);
cmd.Parameters.AddWithValue("$overview", season.Overview);
cmd.Parameters.AddWithValue("$year", season.year);
cmd.Parameters.AddWithValue("$year", season.Year);
cmd.Parameters.AddWithValue("$imgPrimary", season.ImgPrimary);
cmd.Parameters.AddWithValue("$externalIDs", season.ExternalIDs);
cmd.ExecuteNonQuery();
@ -1042,7 +1044,7 @@ namespace Kyoo.InternalAPI
Console.Error.WriteLine("SQL error while trying to insert a season ({0}), season probably already registered.", season.Title);
cmd.CommandText = "SELECT * FROM seasons WHERE showID = $showID AND seasonNumber = $seasonNumber";
cmd.Parameters.AddWithValue("$showID", season.ShowID);
cmd.Parameters.AddWithValue("$seasonNumber", season.seasonNumber);
cmd.Parameters.AddWithValue("$seasonNumber", season.SeasonNumber);
return (long)cmd.ExecuteScalar();
}
}
@ -1057,9 +1059,9 @@ namespace Kyoo.InternalAPI
{
cmd.Parameters.AddWithValue("$showID", episode.ShowID);
cmd.Parameters.AddWithValue("$seasonID", episode.SeasonID);
cmd.Parameters.AddWithValue("$seasonNUmber", episode.seasonNumber);
cmd.Parameters.AddWithValue("$episodeNumber", episode.episodeNumber);
cmd.Parameters.AddWithValue("$absoluteNumber", episode.absoluteNumber);
cmd.Parameters.AddWithValue("$seasonNUmber", episode.SeasonNumber);
cmd.Parameters.AddWithValue("$episodeNumber", episode.EpisodeNumber);
cmd.Parameters.AddWithValue("$absoluteNumber", episode.AbsoluteNumber);
cmd.Parameters.AddWithValue("$path", episode.Path);
cmd.Parameters.AddWithValue("$title", episode.Title);
cmd.Parameters.AddWithValue("$overview", episode.Overview);
@ -1077,8 +1079,8 @@ namespace Kyoo.InternalAPI
Console.Error.WriteLine("SQL error while trying to insert an episode ({0}), episode probably already registered.", episode.Link);
cmd.CommandText = "SELECT * FROM episodes WHERE showID = $showID AND seasonNumber = $seasonNumber AND episodeNumber = $episodeNumber";
cmd.Parameters.AddWithValue("$showID", episode.ShowID);
cmd.Parameters.AddWithValue("$seasonNumber", episode.seasonNumber);
cmd.Parameters.AddWithValue("$episodeNumber", episode.episodeNumber);
cmd.Parameters.AddWithValue("$seasonNumber", episode.SeasonNumber);
cmd.Parameters.AddWithValue("$episodeNumber", episode.EpisodeNumber);
return (long)cmd.ExecuteScalar();
}
}
@ -1089,7 +1091,7 @@ namespace Kyoo.InternalAPI
string query = "INSERT INTO tracks (episodeID, streamType, title, language, codec, isDefault, isForced, isExternal, path) VALUES($episodeID, $streamType, $title, $language, $codec, $isDefault, $isForced, $isExternal, $path);";
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$episodeID", track.episodeID);
cmd.Parameters.AddWithValue("$episodeID", track.EpisodeID);
cmd.Parameters.AddWithValue("$streamType", track.Type);
cmd.Parameters.AddWithValue("$title", track.Title);
cmd.Parameters.AddWithValue("$language", track.Language);
@ -1102,21 +1104,21 @@ namespace Kyoo.InternalAPI
}
}
public void RegisterShowPeople(long showID, List<People> people)
public void RegisterShowPeople(long showID, IEnumerable<People> people)
{
if (people == null)
return;
string linkQuery = "INSERT INTO peopleLinks (peopleID, showID, role, type) VALUES($peopleID, $showID, $role, $type);";
for (int i = 0; i < people.Count; i++)
foreach (People peop in people)
{
using (SQLiteCommand cmd = new SQLiteCommand(linkQuery, sqlConnection))
{
cmd.Parameters.AddWithValue("$peopleID", GetOrCreatePeople(people[i]));
cmd.Parameters.AddWithValue("$peopleID", GetOrCreatePeople(peop));
cmd.Parameters.AddWithValue("$showID", showID);
cmd.Parameters.AddWithValue("$role", people[i].Role);
cmd.Parameters.AddWithValue("$type", people[i].Type);
cmd.Parameters.AddWithValue("$role", peop.Role);
cmd.Parameters.AddWithValue("$type", peop.Type);
cmd.ExecuteNonQuery();
}
}
@ -1164,11 +1166,11 @@ namespace Kyoo.InternalAPI
using (SQLiteCommand cmd = new SQLiteCommand(query, sqlConnection))
{
cmd.Parameters.AddWithValue("$episodeID", episode.id);
cmd.Parameters.AddWithValue("$episodeID", episode.ID);
cmd.ExecuteNonQuery();
}
if (GetEpisodes(episode.ShowID, episode.seasonNumber).Count == 0)
if (GetEpisodes(episode.ShowID, episode.SeasonNumber).Count == 0)
RemoveSeason(episode.ShowID, episode.SeasonID);
}

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.Threading.Tasks;
namespace Kyoo.InternalAPI.ThumbnailsManager
namespace Kyoo.Controllers.ThumbnailsManager
{
public class ThumbnailsManager : IThumbnailsManager
{
@ -20,6 +20,8 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
public async Task<Show> Validate(Show show)
{
if (show == null || show.Path == null)
return null;
string localThumb = Path.Combine(show.Path, "poster.jpg");
string localLogo = Path.Combine(show.Path, "logo.png");
string localBackdrop = Path.Combine(show.Path, "backdrop.jpg");
@ -67,36 +69,39 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
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");
Directory.CreateDirectory(root);
string localThumb = root + "/" + people[i].slug + ".jpg";
if (people[i].imgPrimary != null && !File.Exists(localThumb))
{
string localThumb = root + "/" + peop.Slug + ".jpg";
if (peop.ImgPrimary == null || File.Exists(localThumb))
continue;
try
{
using WebClient client = new WebClient();
await client.DownloadFileTaskAsync(new Uri(people[i].imgPrimary), localThumb);
await client.DownloadFileTaskAsync(new Uri(peop.ImgPrimary), localThumb);
}
catch (WebException)
{
Console.Error.WriteLine("Couldn't download an image.");
}
}
}
return people;
}
public async Task<Episode> Validate(Episode episode)
{
if (episode == null || episode.Path == null)
return null;
string localThumb = Path.ChangeExtension(episode.Path, "jpg");
if (episode.ImgPrimary != null && !File.Exists(localThumb))
{
if (episode.ImgPrimary == null || File.Exists(localThumb))
return episode;
try
{
using WebClient client = new WebClient();
@ -106,7 +111,6 @@ namespace Kyoo.InternalAPI.ThumbnailsManager
{
Console.Error.WriteLine("Couldn't download an image.");
}
}
return episode;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc;
using System.Diagnostics;
@ -23,10 +23,10 @@ namespace Kyoo.Controllers
if (people == null)
return NotFound();
Collection collection = new Collection(0, people.slug, people.Name, null, null)
Collection collection = new Collection(0, people.Slug, people.Name, null, null)
{
Shows = libraryManager.GetShowsByPeople(people.id),
Poster = "peopleimg/" + people.slug
Shows = libraryManager.GetShowsByPeople(people.ID),
Poster = "peopleimg/" + people.Slug
};
return collection;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
using Kyoo.InternalAPI;
using Kyoo.Controllers;
using Kyoo.Models;
using Microsoft.AspNetCore.Mvc;
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>
<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.SpaServices" Version="3.0.0" />
<PackageReference Include="Microsoft.AspNetCore.SpaServices.Extensions" Version="3.0.0" />
@ -29,6 +30,8 @@
<Content Remove="$(SpaRoot)**" />
<None Remove="$(SpaRoot)**" />
<None Include="$(SpaRoot)**" Exclude="$(SpaRoot)node_modules\**" />
<None Remove="Controllers\MetadataProvider\**" />
<Content Remove="Controllers\MetadataProvider\**" />
</ItemGroup>
<ItemGroup>
@ -44,6 +47,15 @@
<EmbeddedResource Include="kyoo.sh">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</EmbeddedResource>
<EmbeddedResource Remove="Controllers\MetadataProvider\**" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Kyoo.Common\Kyoo.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Controllers\MetadataProvider\**" />
</ItemGroup>
<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">
<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_005Clibrarymanager/@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.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
namespace Kyoo
{
public class Program
{
public static void Main(string[] args)
public static async Task Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
IWebHost host = CreateWebHostBuilder(args).Build();
using (IServiceScope serviceScope = host.Services.CreateScope())
{
IPluginManager pluginManager = serviceScope.ServiceProvider.GetService<IPluginManager>();
pluginManager.ReloadPlugins();
ICrawler crawler = serviceScope.ServiceProvider.GetService<ICrawler>();
crawler.Start();
}
await host.RunAsync();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>

View File

@ -1,5 +1,5 @@
using Kyoo.InternalAPI;
using Kyoo.InternalAPI.ThumbnailsManager;
using Kyoo.Controllers;
using Kyoo.Controllers.ThumbnailsManager;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.SpaServices.AngularCli;
@ -30,15 +30,14 @@ namespace Kyoo
});
services.AddControllers().AddNewtonsoftJson();
services.AddHttpClient();
//Services needed in the private and in the public API
services.AddSingleton<ILibraryManager, LibraryManager>();
services.AddSingleton<ITranscoder, Transcoder>();
//Services used to get informations about files and register them
services.AddSingleton<ICrawler, Crawler>();
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.

View File

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